From 51915b86bbfd1a3ed04cceeba6e04d9fab6902b9 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Wed, 20 Nov 2024 13:26:05 +0100 Subject: [PATCH 01/16] update oss --- go.mod | 14 +- go.sum | 36 +- .../inngest/inngest/pkg/consts/otel.go | 2 + .../inngest/inngest/pkg/enums/opcode.go | 1 + .../inngest/pkg/enums/opcode_enumer.go | 12 +- .../inngest/inngest/pkg/event/event.go | 2 +- .../pkg/execution/state/driver_response.go | 315 +------ .../inngest/pkg/execution/state/opcode.go | 334 +++++++ .../inngest/pkg/util/aigateway/aigateway.go | 75 ++ .../inngest/pkg/util/aigateway/request.go | 87 ++ .../sashabaranov/go-openai/.gitignore | 19 + .../sashabaranov/go-openai/.golangci.yml | 272 ++++++ .../sashabaranov/go-openai/CONTRIBUTING.md | 88 ++ .../github.com/sashabaranov/go-openai/LICENSE | 201 +++++ .../sashabaranov/go-openai/README.md | 853 ++++++++++++++++++ .../sashabaranov/go-openai/assistant.go | 325 +++++++ .../sashabaranov/go-openai/audio.go | 234 +++++ .../sashabaranov/go-openai/batch.go | 271 ++++++ .../github.com/sashabaranov/go-openai/chat.go | 409 +++++++++ .../sashabaranov/go-openai/chat_stream.go | 105 +++ .../sashabaranov/go-openai/client.go | 319 +++++++ .../sashabaranov/go-openai/common.go | 24 + .../sashabaranov/go-openai/completion.go | 341 +++++++ .../sashabaranov/go-openai/config.go | 89 ++ .../sashabaranov/go-openai/edits.go | 53 ++ .../sashabaranov/go-openai/embeddings.go | 267 ++++++ .../sashabaranov/go-openai/engines.go | 52 ++ .../sashabaranov/go-openai/error.go | 115 +++ .../sashabaranov/go-openai/files.go | 171 ++++ .../sashabaranov/go-openai/fine_tunes.go | 178 ++++ .../sashabaranov/go-openai/fine_tuning_job.go | 159 ++++ .../sashabaranov/go-openai/image.go | 209 +++++ .../go-openai/internal/error_accumulator.go | 44 + .../go-openai/internal/form_builder.go | 65 ++ .../go-openai/internal/marshaller.go | 15 + .../go-openai/internal/request_builder.go | 52 ++ .../go-openai/internal/unmarshaler.go | 15 + .../sashabaranov/go-openai/messages.go | 218 +++++ .../sashabaranov/go-openai/models.go | 90 ++ .../sashabaranov/go-openai/moderation.go | 107 +++ .../sashabaranov/go-openai/ratelimit.go | 43 + .../github.com/sashabaranov/go-openai/run.go | 453 ++++++++++ .../sashabaranov/go-openai/speech.go | 59 ++ .../sashabaranov/go-openai/stream.go | 55 ++ .../sashabaranov/go-openai/stream_reader.go | 113 +++ .../sashabaranov/go-openai/thread.go | 171 ++++ .../sashabaranov/go-openai/vector_store.go | 348 +++++++ vendor/modules.txt | 29 +- 48 files changed, 7144 insertions(+), 365 deletions(-) create mode 100644 vendor/github.com/inngest/inngest/pkg/execution/state/opcode.go create mode 100644 vendor/github.com/inngest/inngest/pkg/util/aigateway/aigateway.go create mode 100644 vendor/github.com/inngest/inngest/pkg/util/aigateway/request.go create mode 100644 vendor/github.com/sashabaranov/go-openai/.gitignore create mode 100644 vendor/github.com/sashabaranov/go-openai/.golangci.yml create mode 100644 vendor/github.com/sashabaranov/go-openai/CONTRIBUTING.md create mode 100644 vendor/github.com/sashabaranov/go-openai/LICENSE create mode 100644 vendor/github.com/sashabaranov/go-openai/README.md create mode 100644 vendor/github.com/sashabaranov/go-openai/assistant.go create mode 100644 vendor/github.com/sashabaranov/go-openai/audio.go create mode 100644 vendor/github.com/sashabaranov/go-openai/batch.go create mode 100644 vendor/github.com/sashabaranov/go-openai/chat.go create mode 100644 vendor/github.com/sashabaranov/go-openai/chat_stream.go create mode 100644 vendor/github.com/sashabaranov/go-openai/client.go create mode 100644 vendor/github.com/sashabaranov/go-openai/common.go create mode 100644 vendor/github.com/sashabaranov/go-openai/completion.go create mode 100644 vendor/github.com/sashabaranov/go-openai/config.go create mode 100644 vendor/github.com/sashabaranov/go-openai/edits.go create mode 100644 vendor/github.com/sashabaranov/go-openai/embeddings.go create mode 100644 vendor/github.com/sashabaranov/go-openai/engines.go create mode 100644 vendor/github.com/sashabaranov/go-openai/error.go create mode 100644 vendor/github.com/sashabaranov/go-openai/files.go create mode 100644 vendor/github.com/sashabaranov/go-openai/fine_tunes.go create mode 100644 vendor/github.com/sashabaranov/go-openai/fine_tuning_job.go create mode 100644 vendor/github.com/sashabaranov/go-openai/image.go create mode 100644 vendor/github.com/sashabaranov/go-openai/internal/error_accumulator.go create mode 100644 vendor/github.com/sashabaranov/go-openai/internal/form_builder.go create mode 100644 vendor/github.com/sashabaranov/go-openai/internal/marshaller.go create mode 100644 vendor/github.com/sashabaranov/go-openai/internal/request_builder.go create mode 100644 vendor/github.com/sashabaranov/go-openai/internal/unmarshaler.go create mode 100644 vendor/github.com/sashabaranov/go-openai/messages.go create mode 100644 vendor/github.com/sashabaranov/go-openai/models.go create mode 100644 vendor/github.com/sashabaranov/go-openai/moderation.go create mode 100644 vendor/github.com/sashabaranov/go-openai/ratelimit.go create mode 100644 vendor/github.com/sashabaranov/go-openai/run.go create mode 100644 vendor/github.com/sashabaranov/go-openai/speech.go create mode 100644 vendor/github.com/sashabaranov/go-openai/stream.go create mode 100644 vendor/github.com/sashabaranov/go-openai/stream_reader.go create mode 100644 vendor/github.com/sashabaranov/go-openai/thread.go create mode 100644 vendor/github.com/sashabaranov/go-openai/vector_store.go diff --git a/go.mod b/go.mod index 76409fa4..554d672e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gosimple/slug v1.12.0 github.com/gowebpki/jcs v1.0.0 - github.com/inngest/inngest v1.1.1-beta.1.0.20241119190743-29bd75bd8025 + github.com/inngest/inngest v1.2.1-0.20241120122427-5ae5a84711e0 github.com/oklog/ulid/v2 v2.1.0 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/stretchr/testify v1.9.0 @@ -18,16 +18,12 @@ require ( ) require ( - github.com/99designs/gqlgen v0.17.27 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect - github.com/agnivade/levenshtein v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dmarkham/enumer v1.5.8 // indirect github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -49,7 +45,6 @@ require ( github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/ohler55/ojg v1.24.1 // indirect - github.com/pascaldekloe/name v1.0.1 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -59,7 +54,7 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/zerolog v1.26.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sashabaranov/go-openai v1.35.6 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.4.1 // indirect @@ -69,9 +64,6 @@ require ( github.com/stoewer/go-strcase v1.3.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/tidwall/btree v1.7.0 // indirect - github.com/urfave/cli/v2 v2.25.1 // indirect - github.com/vektah/gqlparser/v2 v2.5.15 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect @@ -88,12 +80,10 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.28.0 // indirect - golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect - golang.org/x/tools v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/grpc v1.67.1 // indirect diff --git a/go.sum b/go.sum index b488436d..fbe15eb9 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,7 @@ -github.com/99designs/gqlgen v0.17.27 h1:XPsaZiWY1lL2qqVYtBt37GzkyX7bBiVvda7k1buC/Ao= -github.com/99designs/gqlgen v0.17.27/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= -github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= -github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -16,14 +11,9 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM= -github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak= github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY= github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -59,14 +49,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inngest/expr v0.0.0-20241106234328-863dff7deec0 h1:cqaGD0mx745BYyVGZ3GJsiZETktC64h1r9zdDInmKIs= github.com/inngest/expr v0.0.0-20241106234328-863dff7deec0/go.mod h1:0Dllw9clwlMWWxfiSsHY9VdE+Zjt/8SVCMxK9r39aKE= -github.com/inngest/inngest v1.1.1-beta.1.0.20241119174848-e02d13155773 h1:mrybHpNFPz6WdC8erNn9B0gQNHvv78rkK64mrnrWuNs= -github.com/inngest/inngest v1.1.1-beta.1.0.20241119174848-e02d13155773/go.mod h1:PPRdEsuI/lsU4YM24pmssJ/yM3ImFuW6uvwD+5hRKLg= -github.com/inngest/inngest v1.1.1-beta.1.0.20241119184343-b37f436bb2de h1:otbFZCkgId4GSWTFb3yJJKBsRQaoX8OzEQyanizv3k8= -github.com/inngest/inngest v1.1.1-beta.1.0.20241119184343-b37f436bb2de/go.mod h1:PPRdEsuI/lsU4YM24pmssJ/yM3ImFuW6uvwD+5hRKLg= -github.com/inngest/inngest v1.1.1-beta.1.0.20241119185653-d0f858cc8a5d h1:j2ZJperqeLpMHNN4iKogxpKpkJ7qEesHmwvqPCzjmaQ= -github.com/inngest/inngest v1.1.1-beta.1.0.20241119185653-d0f858cc8a5d/go.mod h1:PPRdEsuI/lsU4YM24pmssJ/yM3ImFuW6uvwD+5hRKLg= -github.com/inngest/inngest v1.1.1-beta.1.0.20241119190743-29bd75bd8025 h1:zzXR9RrXnbHOVd8EhvX1FWSnoBmzHUAcSsizMDEPmeU= -github.com/inngest/inngest v1.1.1-beta.1.0.20241119190743-29bd75bd8025/go.mod h1:PPRdEsuI/lsU4YM24pmssJ/yM3ImFuW6uvwD+5hRKLg= +github.com/inngest/inngest v1.2.1-0.20241120122427-5ae5a84711e0 h1:/J6q4xMwYvPRGy2Pz4e5sCrw79eq6yIGq4EDwujdON0= +github.com/inngest/inngest v1.2.1-0.20241120122427-5ae5a84711e0/go.mod h1:+Nl49tpdy+gRzEYHgEb3eNV7aheh8iXyP15XKc+nVU4= github.com/karlseguin/ccache/v2 v2.0.8 h1:lT38cE//uyf6KcFok0rlgXtGFBWxkI6h/qg4tbFyDnA= github.com/karlseguin/ccache/v2 v2.0.8/go.mod h1:2BDThcfQMf/c0jnZowt16eW405XIqZPavt+HoYEtcxQ= github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003 h1:vJ0Snvo+SLMY72r5J4sEfkuE7AFbixEP2qRbEcum/wA= @@ -94,8 +78,6 @@ github.com/ohler55/ojg v1.24.1 h1:PaVLelrNgT5/0ppPaUtey54tOVp245z33fkhL2jljjY= github.com/ohler55/ojg v1.24.1/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0= -github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= @@ -122,8 +104,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sashabaranov/go-openai v1.35.6 h1:oi0rwCvyxMxgFALDGnyqFTyCJm6n72OnEG3sybIFR0g= +github.com/sashabaranov/go-openai v1.35.6/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= @@ -155,16 +137,10 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= -github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= -github.com/vektah/gqlparser/v2 v2.5.15 h1:fYdnU8roQniJziV5TDiFPm/Ff7pE8xbVSOJqbsdl88A= -github.com/vektah/gqlparser/v2 v2.5.15/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= @@ -207,8 +183,6 @@ golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -239,8 +213,6 @@ golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/github.com/inngest/inngest/pkg/consts/otel.go b/vendor/github.com/inngest/inngest/pkg/consts/otel.go index fb083d37..dc830ea6 100644 --- a/vendor/github.com/inngest/inngest/pkg/consts/otel.go +++ b/vendor/github.com/inngest/inngest/pkg/consts/otel.go @@ -57,6 +57,8 @@ const ( OtelSysStepFirst = "sys.step.first" OtelSysStepGroupID = "sys.step.group.id" OtelSysStepStack = "sys.step.stack" + OtelSysStepAIRequest = "sys.step.ai.req" // ai request metadata + OtelSysStepAIResponse = "sys.step.ai.res" // ai response metadata OtelSysStepRunType = "sys.step.run.type" OtelSysStepSleepEndAt = "sys.step.sleep.end" diff --git a/vendor/github.com/inngest/inngest/pkg/enums/opcode.go b/vendor/github.com/inngest/inngest/pkg/enums/opcode.go index e46343d8..86a7a1cc 100644 --- a/vendor/github.com/inngest/inngest/pkg/enums/opcode.go +++ b/vendor/github.com/inngest/inngest/pkg/enums/opcode.go @@ -14,4 +14,5 @@ const ( OpcodeSleep OpcodeWaitForEvent OpcodeInvokeFunction + OpcodeAIGateway // AI gateway inference call ) diff --git a/vendor/github.com/inngest/inngest/pkg/enums/opcode_enumer.go b/vendor/github.com/inngest/inngest/pkg/enums/opcode_enumer.go index d7080cab..4518955e 100644 --- a/vendor/github.com/inngest/inngest/pkg/enums/opcode_enumer.go +++ b/vendor/github.com/inngest/inngest/pkg/enums/opcode_enumer.go @@ -8,11 +8,11 @@ import ( "strings" ) -const _OpcodeName = "NoneStepStepRunStepErrorStepPlannedSleepWaitForEventInvokeFunction" +const _OpcodeName = "NoneStepStepRunStepErrorStepPlannedSleepWaitForEventInvokeFunctionAIGateway" -var _OpcodeIndex = [...]uint8{0, 4, 8, 15, 24, 35, 40, 52, 66} +var _OpcodeIndex = [...]uint8{0, 4, 8, 15, 24, 35, 40, 52, 66, 75} -const _OpcodeLowerName = "nonestepsteprunsteperrorstepplannedsleepwaitforeventinvokefunction" +const _OpcodeLowerName = "nonestepsteprunsteperrorstepplannedsleepwaitforeventinvokefunctionaigateway" func (i Opcode) String() string { if i < 0 || i >= Opcode(len(_OpcodeIndex)-1) { @@ -33,9 +33,10 @@ func _OpcodeNoOp() { _ = x[OpcodeSleep-(5)] _ = x[OpcodeWaitForEvent-(6)] _ = x[OpcodeInvokeFunction-(7)] + _ = x[OpcodeAIGateway-(8)] } -var _OpcodeValues = []Opcode{OpcodeNone, OpcodeStep, OpcodeStepRun, OpcodeStepError, OpcodeStepPlanned, OpcodeSleep, OpcodeWaitForEvent, OpcodeInvokeFunction} +var _OpcodeValues = []Opcode{OpcodeNone, OpcodeStep, OpcodeStepRun, OpcodeStepError, OpcodeStepPlanned, OpcodeSleep, OpcodeWaitForEvent, OpcodeInvokeFunction, OpcodeAIGateway} var _OpcodeNameToValueMap = map[string]Opcode{ _OpcodeName[0:4]: OpcodeNone, @@ -54,6 +55,8 @@ var _OpcodeNameToValueMap = map[string]Opcode{ _OpcodeLowerName[40:52]: OpcodeWaitForEvent, _OpcodeName[52:66]: OpcodeInvokeFunction, _OpcodeLowerName[52:66]: OpcodeInvokeFunction, + _OpcodeName[66:75]: OpcodeAIGateway, + _OpcodeLowerName[66:75]: OpcodeAIGateway, } var _OpcodeNames = []string{ @@ -65,6 +68,7 @@ var _OpcodeNames = []string{ _OpcodeName[35:40], _OpcodeName[40:52], _OpcodeName[52:66], + _OpcodeName[66:75], } // OpcodeString retrieves an enum value from the enum constants string name. diff --git a/vendor/github.com/inngest/inngest/pkg/event/event.go b/vendor/github.com/inngest/inngest/pkg/event/event.go index cb84821a..2f60edec 100644 --- a/vendor/github.com/inngest/inngest/pkg/event/event.go +++ b/vendor/github.com/inngest/inngest/pkg/event/event.go @@ -247,7 +247,7 @@ func (o ossTrackedEvent) GetInternalID() ulid.ULID { func (o ossTrackedEvent) GetWorkspaceID() uuid.UUID { // There are no workspaces in OSS yet. - return uuid.UUID{} + return consts.DevServerEnvId } type NewInvocationEventOpts struct { diff --git a/vendor/github.com/inngest/inngest/pkg/execution/state/driver_response.go b/vendor/github.com/inngest/inngest/pkg/execution/state/driver_response.go index dcee0dae..172fbd63 100644 --- a/vendor/github.com/inngest/inngest/pkg/execution/state/driver_response.go +++ b/vendor/github.com/inngest/inngest/pkg/execution/state/driver_response.go @@ -7,11 +7,8 @@ import ( "strconv" "time" - "github.com/inngest/inngest/pkg/dateutil" "github.com/inngest/inngest/pkg/enums" - "github.com/inngest/inngest/pkg/event" "github.com/inngest/inngest/pkg/inngest" - "github.com/xhit/go-str2duration/v2" ) const DefaultErrorName = "Error" @@ -31,7 +28,7 @@ type UserError struct { // the SDK MAY choose to store additional data for its own purposes here. Data json.RawMessage `json:"data,omitempty"` - // NoRetry is set when parsing the opcide via the retry header. + // NoRetry is set when parsing the opcode via the retry header. // It is NOT set via the SDK. NoRetry bool `json:"noRetry,omitempty"` @@ -39,295 +36,6 @@ type UserError struct { Cause *UserError `json:"cause,omitempty"` } -type GeneratorOpcode struct { - // Op represents the type of operation invoked in the function. - Op enums.Opcode `json:"op"` - // ID represents a hashed unique ID for the operation. This acts - // as the generated step ID for the state store. - ID string `json:"id"` - // Name represents the name of the step, or the sleep duration for - // sleeps. - Name string `json:"name"` - // Opts indicate options for the operation, eg. matching expressions - // when setting up async event listeners via `waitForEvent`, or retry - // policies for steps. - Opts any `json:"opts"` - // Data is the resulting data from the operation, eg. the step - // output. - Data json.RawMessage `json:"data"` - // Error is the failing result from the operation, e.g. an error thrown - // from a step. This MUST be in the shape of OpcodeError. - Error *UserError `json:"error"` - // SDK versions < 3.?.? don't respond with the display name. - DisplayName *string `json:"displayName"` -} - -// Get the name of the step as defined in code by the user. -func (g GeneratorOpcode) UserDefinedName() string { - if g.DisplayName != nil { - return *g.DisplayName - } - - // SDK versions < 3.?.? don't respond with the display - // name, so we we'll use the deprecated name field as a - // fallback. - return g.Name -} - -// Get the stringified input of the step. -func (g GeneratorOpcode) Input() (string, error) { - runOpts, _ := g.RunOpts() - if runOpts != nil && runOpts.Input != nil { - return string(runOpts.Input), nil - } - - // Failure to unwrap run opts just means this isn't an op that can provide - // input, so ignore. - return "", nil - -} - -// Get the stringified output of the step. -func (g GeneratorOpcode) Output() (string, error) { - // OpcodeStepError MUST always wrap the output in an "error" - // field, allowing the SDK to differentiate between an error and data. - if g.Op == enums.OpcodeStepError { - byt, err := json.Marshal(map[string]any{"error": g.Error}) - return string(byt), err - } - - // If this is an OpcodeStepRun, we can guarantee that the data is unwrapped. - // - // We MUST wrap the data in a "data" object in the state store so that the - // SDK can differentiate between "data" and "error"; per-step errors wraps the - // error with "error" and updates step state on the final failure. - if g.Op == enums.OpcodeStepRun { - byt, err := json.Marshal(map[string]any{"data": g.Data}) - return string(byt), err - } - - // Data is allowed to be `null` if no error is found and the op returned no data. - if g.Data != nil { - return string(g.Data), nil - } - return "", nil -} - -// IsError returns whether this op represents an error, for example a -// `StepError` being passed back from an SDK. -func (g GeneratorOpcode) IsError() bool { - return g.Error != nil -} - -func (g GeneratorOpcode) RunOpts() (*RunOpts, error) { - opts := &RunOpts{} - if err := opts.UnmarshalAny(g.Opts); err != nil { - return nil, err - } - return opts, nil -} - -// Returns, if any, the type of a StepRun operation. -func (g GeneratorOpcode) RunType() string { - opts, err := g.RunOpts() - if err != nil { - return "" - } - return opts.Type -} - -func (g GeneratorOpcode) WaitForEventOpts() (*WaitForEventOpts, error) { - if opts, ok := g.Opts.(*WaitForEventOpts); ok && opts != nil { - return opts, nil - } - - opts := &WaitForEventOpts{} - if err := opts.UnmarshalAny(g.Opts); err != nil { - return nil, err - } - if opts.Event == "" { - // use the step name as a fallback, for v1/2 of the TS SDK. - opts.Event = g.Name - } - if opts.Event == "" { - return nil, fmt.Errorf("An event name must be provided when waiting for an event") - } - return opts, nil -} - -func (g GeneratorOpcode) SleepDuration() (time.Duration, error) { - if g.Op != enums.OpcodeSleep { - return 0, fmt.Errorf("unable to return sleep duration for opcode %s", g.Op.String()) - } - - opts := &SleepOpts{} - if err := opts.UnmarshalAny(g.Opts); err != nil { - return 0, err - } - - if opts.Duration == "" { - // use step name as a fallback for v1/2 of the TS SDK - opts.Duration = g.Name - } - if len(opts.Duration) == 0 { - return 0, nil - } - - // Quick heuristic to check if this is likely a date layout - if len(opts.Duration) >= 10 { - if parsed, err := dateutil.Parse(opts.Duration); err == nil { - at := time.Until(parsed).Round(time.Second) - if at < 0 { - return time.Duration(0), nil - } - return at, nil - } - } - - return str2duration.ParseDuration(opts.Duration) -} - -func (g GeneratorOpcode) InvokeFunctionOpts() (*InvokeFunctionOpts, error) { - opts := &InvokeFunctionOpts{} - if err := opts.UnmarshalAny(g.Opts); err != nil { - return nil, err - } - return opts, nil -} - -type InvokeFunctionOpts struct { - FunctionID string `json:"function_id"` - Payload *event.Event `json:"payload,omitempty"` - Timeout string `json:"timeout"` -} - -func (i *InvokeFunctionOpts) UnmarshalAny(a any) error { - opts := InvokeFunctionOpts{} - var mappedByt []byte - switch typ := a.(type) { - case []byte: - mappedByt = typ - default: - byt, err := json.Marshal(a) - if err != nil { - return err - } - mappedByt = byt - } - if err := json.Unmarshal(mappedByt, &opts); err != nil { - return err - } - *i = opts - return nil -} - -func (i InvokeFunctionOpts) Expires() (time.Time, error) { - if i.Timeout == "" { - return time.Now().AddDate(1, 0, 0), nil - } - - dur, err := str2duration.ParseDuration(i.Timeout) - if err != nil { - return time.Time{}, err - } - return time.Now().Add(dur), nil -} - -type SleepOpts struct { - Duration string `json:"duration"` -} - -func (s *SleepOpts) UnmarshalAny(a any) error { - opts := SleepOpts{} - var mappedByt []byte - switch typ := a.(type) { - case []byte: - mappedByt = typ - default: - byt, err := json.Marshal(a) - if err != nil { - return err - } - mappedByt = byt - } - if err := json.Unmarshal(mappedByt, &opts); err != nil { - return err - } - *s = opts - return nil -} - -type RunOpts struct { - Type string `json:"type,omitempty"` - Input json.RawMessage `json:"input"` -} - -func (r *RunOpts) UnmarshalAny(a any) error { - opts := RunOpts{} - var mappedByt []byte - switch typ := a.(type) { - case []byte: - mappedByt = typ - default: - byt, err := json.Marshal(a) - if err != nil { - return err - } - mappedByt = byt - } - if err := json.Unmarshal(mappedByt, &opts); err != nil { - return err - } - - if len(opts.Input) > 0 && opts.Input[0] != '[' { - return fmt.Errorf("input must be an array or undefined") - } - - *r = opts - return nil -} - -type WaitForEventOpts struct { - Timeout string `json:"timeout"` - If *string `json:"if"` - // Event is taken from GeneratorOpcode.Name if this is empty. - Event string `json:"event"` -} - -func (w *WaitForEventOpts) UnmarshalAny(a any) error { - opts := WaitForEventOpts{} - var mappedByt []byte - switch typ := a.(type) { - case []byte: - mappedByt = typ - default: - byt, err := json.Marshal(a) - if err != nil { - return err - } - mappedByt = byt - } - if err := json.Unmarshal(mappedByt, &opts); err != nil { - return err - } - *w = opts - return nil -} - -func (w WaitForEventOpts) Expires() (time.Time, error) { - if w.Timeout == "" { - // The TypeScript SDK sets timeout to an empty string when the duration - // is negative - return time.Now(), nil - } - - dur, err := str2duration.ParseDuration(w.Timeout) - if err != nil { - return time.Time{}, err - } - return time.Now().Add(dur), nil -} - // DriverResponse is returned after a driver executes an action. This represents any // output from running the step, including the output (as a JSON map), the error, and // whether the driver's response is "scheduled", eg. the driver is running the job @@ -541,6 +249,27 @@ func (r *DriverResponse) IsTraceVisibleFunctionExecution() bool { return r.StatusCode != 206 } +func (r *DriverResponse) UpdateOpcodeOutput(op *GeneratorOpcode, to json.RawMessage) { + for n, o := range r.Generator { + if o.ID != op.ID { + continue + } + op.Data = to + r.Generator[n].Data = to + } +} + +// UpdateOpcodeError updates an opcode's data and error to the given inputs. +func (r *DriverResponse) UpdateOpcodeError(op *GeneratorOpcode, err UserError) { + for n, o := range r.Generator { + if o.ID != op.ID { + continue + } + op.Error = &err + r.Generator[n].Error = &err + } +} + type WrappedStandardError struct { err error diff --git a/vendor/github.com/inngest/inngest/pkg/execution/state/opcode.go b/vendor/github.com/inngest/inngest/pkg/execution/state/opcode.go new file mode 100644 index 00000000..a9afb4f6 --- /dev/null +++ b/vendor/github.com/inngest/inngest/pkg/execution/state/opcode.go @@ -0,0 +1,334 @@ +package state + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/inngest/inngest/pkg/dateutil" + "github.com/inngest/inngest/pkg/enums" + "github.com/inngest/inngest/pkg/event" + "github.com/inngest/inngest/pkg/util/aigateway" + "github.com/xhit/go-str2duration/v2" +) + +type GeneratorOpcode struct { + // Op represents the type of operation invoked in the function. + Op enums.Opcode `json:"op"` + // ID represents a hashed unique ID for the operation. This acts + // as the generated step ID for the state store. + ID string `json:"id"` + // Name represents the name of the step, or the sleep duration for + // sleeps. + Name string `json:"name"` + // Opts indicate options for the operation, eg. matching expressions + // when setting up async event listeners via `waitForEvent`, or retry + // policies for steps. + Opts any `json:"opts"` + // Data is the resulting data from the operation, eg. the step + // output. Note that for gateway requests, this is initially the + // request input. + Data json.RawMessage `json:"data"` + // Error is the failing result from the operation, e.g. an error thrown + // from a step. This MUST be in the shape of OpcodeError. + Error *UserError `json:"error"` + // SDK versions < 3.?.? don't respond with the display name. + DisplayName *string `json:"displayName"` +} + +// Get the name of the step as defined in code by the user. +func (g GeneratorOpcode) UserDefinedName() string { + if g.DisplayName != nil { + return *g.DisplayName + } + + // SDK versions < 3.?.? don't respond with the display + // name, so we we'll use the deprecated name field as a + // fallback. + return g.Name +} + +// Get the stringified input of the step, if `step.Run` was passed inputs +// or if we're using request offloading. +func (g GeneratorOpcode) Input() (string, error) { + // Only specific operations can have inputs. These are currently limited + // to OpcodeStepRun and OpcodeStepAIGateway. + switch g.Op { + case enums.OpcodeStepRun, enums.OpcodeStep: + runOpts, _ := g.RunOpts() + if runOpts != nil && runOpts.Input != nil { + return string(runOpts.Input), nil + } + case enums.OpcodeAIGateway: + req, _ := g.AIGatewayOpts() + return string(req.Body), nil + } + + return "", nil +} + +// Get the stringified output of the step. +func (g GeneratorOpcode) Output() (string, error) { + // OpcodeStepError MUST always wrap the output in an "error" + // field, allowing the SDK to differentiate between an error and data. + if g.Op == enums.OpcodeStepError { + byt, err := json.Marshal(map[string]any{"error": g.Error}) + return string(byt), err + } + + // If this is an OpcodeStepRun, we can guarantee that the data is unwrapped. + // + // We MUST wrap the data in a "data" object in the state store so that the + // SDK can differentiate between "data" and "error"; per-step errors wraps the + // error with "error" and updates step state on the final failure. + if g.Op == enums.OpcodeStepRun { + byt, err := json.Marshal(map[string]any{"data": g.Data}) + return string(byt), err + } + + // Data is allowed to be `null` if no error is found and the op returned no data. + if g.Data != nil { + return string(g.Data), nil + } + return "", nil +} + +// IsError returns whether this op represents an error, for example a +// `StepError` being passed back from an SDK. +func (g GeneratorOpcode) IsError() bool { + return g.Error != nil +} + +// Returns, if any, the type of a StepRun operation. +func (g GeneratorOpcode) RunType() string { + opts, err := g.RunOpts() + if err != nil { + return "" + } + return opts.Type +} + +func (g GeneratorOpcode) RunOpts() (*RunOpts, error) { + opts := &RunOpts{} + if err := opts.UnmarshalAny(g.Opts); err != nil { + return nil, err + } + return opts, nil +} + +func (g GeneratorOpcode) WaitForEventOpts() (*WaitForEventOpts, error) { + if opts, ok := g.Opts.(*WaitForEventOpts); ok && opts != nil { + return opts, nil + } + + opts := &WaitForEventOpts{} + if err := opts.UnmarshalAny(g.Opts); err != nil { + return nil, err + } + if opts.Event == "" { + // use the step name as a fallback, for v1/2 of the TS SDK. + opts.Event = g.Name + } + if opts.Event == "" { + return nil, fmt.Errorf("An event name must be provided when waiting for an event") + } + return opts, nil +} + +func (g GeneratorOpcode) SleepDuration() (time.Duration, error) { + if g.Op != enums.OpcodeSleep { + return 0, fmt.Errorf("unable to return sleep duration for opcode %s", g.Op.String()) + } + + opts := &SleepOpts{} + if err := opts.UnmarshalAny(g.Opts); err != nil { + return 0, err + } + + if opts.Duration == "" { + // use step name as a fallback for v1/2 of the TS SDK + opts.Duration = g.Name + } + if len(opts.Duration) == 0 { + return 0, nil + } + + // Quick heuristic to check if this is likely a date layout + if len(opts.Duration) >= 10 { + if parsed, err := dateutil.Parse(opts.Duration); err == nil { + at := time.Until(parsed).Round(time.Second) + if at < 0 { + return time.Duration(0), nil + } + return at, nil + } + } + + return str2duration.ParseDuration(opts.Duration) +} + +func (g GeneratorOpcode) InvokeFunctionOpts() (*InvokeFunctionOpts, error) { + opts := &InvokeFunctionOpts{} + if err := opts.UnmarshalAny(g.Opts); err != nil { + return nil, err + } + return opts, nil +} + +type InvokeFunctionOpts struct { + FunctionID string `json:"function_id"` + Payload *event.Event `json:"payload,omitempty"` + Timeout string `json:"timeout"` +} + +func (i *InvokeFunctionOpts) UnmarshalAny(a any) error { + opts := InvokeFunctionOpts{} + var mappedByt []byte + switch typ := a.(type) { + case []byte: + mappedByt = typ + default: + byt, err := json.Marshal(a) + if err != nil { + return err + } + mappedByt = byt + } + if err := json.Unmarshal(mappedByt, &opts); err != nil { + return err + } + *i = opts + return nil +} + +func (i InvokeFunctionOpts) Expires() (time.Time, error) { + if i.Timeout == "" { + return time.Now().AddDate(1, 0, 0), nil + } + + dur, err := str2duration.ParseDuration(i.Timeout) + if err != nil { + return time.Time{}, err + } + return time.Now().Add(dur), nil +} + +type SleepOpts struct { + Duration string `json:"duration"` +} + +func (s *SleepOpts) UnmarshalAny(a any) error { + opts := SleepOpts{} + var mappedByt []byte + switch typ := a.(type) { + case []byte: + mappedByt = typ + default: + byt, err := json.Marshal(a) + if err != nil { + return err + } + mappedByt = byt + } + if err := json.Unmarshal(mappedByt, &opts); err != nil { + return err + } + *s = opts + return nil +} + +type RunOpts struct { + Type string `json:"type,omitempty"` + Input json.RawMessage `json:"input"` +} + +func (r *RunOpts) UnmarshalAny(a any) error { + opts := RunOpts{} + var mappedByt []byte + switch typ := a.(type) { + case []byte: + mappedByt = typ + default: + byt, err := json.Marshal(a) + if err != nil { + return err + } + mappedByt = byt + } + if err := json.Unmarshal(mappedByt, &opts); err != nil { + return err + } + + if len(opts.Input) > 0 && opts.Input[0] != '[' { + return fmt.Errorf("input must be an array or undefined") + } + + *r = opts + return nil +} + +type WaitForEventOpts struct { + Timeout string `json:"timeout"` + If *string `json:"if"` + // Event is taken from GeneratorOpcode.Name if this is empty. + Event string `json:"event"` +} + +func (w *WaitForEventOpts) UnmarshalAny(a any) error { + opts := WaitForEventOpts{} + var mappedByt []byte + switch typ := a.(type) { + case []byte: + mappedByt = typ + default: + byt, err := json.Marshal(a) + if err != nil { + return err + } + mappedByt = byt + } + if err := json.Unmarshal(mappedByt, &opts); err != nil { + return err + } + *w = opts + return nil +} + +func (w WaitForEventOpts) Expires() (time.Time, error) { + if w.Timeout == "" { + // The TypeScript SDK sets timeout to an empty string when the duration + // is negative + return time.Now(), nil + } + + dur, err := str2duration.ParseDuration(w.Timeout) + if err != nil { + return time.Time{}, err + } + return time.Now().Add(dur), nil +} + +// AIGatewayOpts returns the AI gateway options within the driver. +func (g *GeneratorOpcode) AIGatewayOpts() (aigateway.Request, error) { + req := aigateway.Request{} + + // Ensure we unmarshal g.Opts into the request options. + // This contains Inngest-related and auth-related options + // that do not go in the API request body we make to the provider + var optByt []byte + switch typ := g.Opts.(type) { + case []byte: + optByt = typ + default: + var err error + optByt, err = json.Marshal(g.Opts) + if err != nil { + return aigateway.Request{}, err + } + } + if err := json.Unmarshal(optByt, &req); err != nil { + return aigateway.Request{}, err + } + + return req, nil +} diff --git a/vendor/github.com/inngest/inngest/pkg/util/aigateway/aigateway.go b/vendor/github.com/inngest/inngest/pkg/util/aigateway/aigateway.go new file mode 100644 index 00000000..e61dd637 --- /dev/null +++ b/vendor/github.com/inngest/inngest/pkg/util/aigateway/aigateway.go @@ -0,0 +1,75 @@ +package aigateway + +import ( + "context" + "encoding/json" + "fmt" + "net/url" +) + +// ParsedRequest represents the parsed request data for a given inference request. +// +// Note that this is not stored, and instead is computed just in time for each input +// depending on the UI. +type ParsedInferenceRequest struct { + URL url.URL `json:"url"` + Model string `json:"model"` + Seed *int `json:"seed,omitempty"` + Temprature float32 `json:"temperature,omitempty"` + TopP float32 `json:"top_p,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` + MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` + StopSequences []string `json:"stop,omitempty"` + // TODO: Input messages and so on. + // TODO: Tools +} + +// ParsedInferenceResponse represents the parsed output for a given inference request. +type ParsedInferenceResponse struct { + ID string `json:"id"` + TokensIn int32 `json:"tokens_in"` + TokensOut int32 `json:"tokens_out"` + StopReason string `json:"stop_reason"` + // TODO: Tool use selections, parsed. +} + +func ParseUnknownInput(ctx context.Context, req json.RawMessage) (ParsedInferenceRequest, error) { + return ParsedInferenceRequest{}, fmt.Errorf("todo") +} + +// Parse validates an inference request. This checks which model, format, and URLs we should +// use for the given request which is passed through to the end provider. +// +// By default, we support the following: +// +// * OpenAI (and OpenAI compatible URLs, by changing the base URL) +// * Anthropic +// * Bedrock +// * Google Generative AI +// * Mistral +// * Cohere +// * Groq (must include URL) +func ParseInput(ctx context.Context, req Request) (ParsedInferenceRequest, error) { + switch req.Format { + case FormatOpenAIChat: + // OpenAI Chat is the default format, so fall through to the default. + fallthrough + default: + // Parse everything as an OpenAI Chat request. + rf := RFOpenAIChatCompletion{} + if err := json.Unmarshal(req.Body, &rf); err != nil { + return ParsedInferenceRequest{}, err + } + + return ParsedInferenceRequest{ + Model: rf.Model, + Seed: rf.Seed, + Temprature: rf.Temperature, + TopP: rf.TopP, + MaxTokens: rf.MaxTokens, + MaxCompletionTokens: rf.MaxCompletionTokens, + StopSequences: rf.Stop, + }, nil + } + +} diff --git a/vendor/github.com/inngest/inngest/pkg/util/aigateway/request.go b/vendor/github.com/inngest/inngest/pkg/util/aigateway/request.go new file mode 100644 index 00000000..3a40b6a1 --- /dev/null +++ b/vendor/github.com/inngest/inngest/pkg/util/aigateway/request.go @@ -0,0 +1,87 @@ +package aigateway + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + // NOTE: We don't use the default `openai` package because Stainless SDKs don't + // support Unmarshal() on Param structs, due to their Field handling. + // See https://play.golang.com/p/UX60snrf3gp for more info. + openai "github.com/sashabaranov/go-openai" +) + +const ( + // FormatOpenAIChat represents the default OpenAI chat completion request. + FormatOpenAIChat = "openai-chat" + FormatAnthropic = "anthropic" + FormatGemini = "gemini" + FormatBedrock = "bedrock" +) + +type Request struct { + // URL is the full endpoint that we're sending the request to. This must + // always be provided by our SDKs. + URL string `json:"url,omitempty"` + // Headers represent additional headers to send in the request. + Headers map[string]string `json:"headers,omitempty"` + // AuthKey is an API key to be sent with the request. This contains + // API tokens which are never logged. + AuthKey string `json:"auth_key,omitempty"` + // AutoToolCall indicates whether the request should automatically invoke functions + // when using inngest functions as tools. This allows us to immediately execute without + // round trips. + AutoToolCall bool `json:"auto_tool_call"` + // Format represents the request format type, eg. an OpenAI compatible endpoint + // request, or a Groq request. + Format string `json:"format"` + // Body indicates the raw content of the request, as a slice of JSON bytes. + // It's expected that this comes from our SDKs directly. + Body json.RawMessage `json:"body"` +} + +func (r Request) MarshalJSON() ([]byte, error) { + // Do not allow this to be marshalled. We do not want the auth creds to + // be logged. + return nil, nil +} + +func (r Request) HTTPRequest() (*http.Request, error) { + req, err := http.NewRequest(http.MethodPost, r.URL, bytes.NewReader(r.Body)) + if err != nil { + return nil, err + } + + // Always sending JSON. + req.Header.Add("content-type", "application/json") + + // Add auth, depending on the format. + switch r.Format { + case FormatGemini: + // Gemini adds the auth key as a query param + values := req.URL.Query() + values.Add("key", r.AuthKey) + req.URL.RawQuery = values.Encode() + case FormatBedrock: + // Bedrock's auth should be the fully-generated AWS key derived from the + // secret and signing key. + req.Header.Add("Authorization", r.AuthKey) + case FormatAnthropic: + // Anthropic uses a non-standard header. + req.Header.Add("x-api-key", r.AuthKey) + default: + // By default, use standards. + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.AuthKey)) + } + + // Overwrite any headers if custom headers are added to opts. + for header, val := range r.Headers { + req.Header.Add(header, val) + } + + return req, nil +} + +// RFOpenAIChatCompletion represents an OpenAI compatible format. +type RFOpenAIChatCompletion openai.ChatCompletionRequest diff --git a/vendor/github.com/sashabaranov/go-openai/.gitignore b/vendor/github.com/sashabaranov/go-openai/.gitignore new file mode 100644 index 00000000..99b40bf1 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/.gitignore @@ -0,0 +1,19 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Auth token for tests +.openai-token +.idea \ No newline at end of file diff --git a/vendor/github.com/sashabaranov/go-openai/.golangci.yml b/vendor/github.com/sashabaranov/go-openai/.golangci.yml new file mode 100644 index 00000000..58fab4a2 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/.golangci.yml @@ -0,0 +1,272 @@ +## Golden config for golangci-lint v1.47.3 +# +# This is the best config for golangci-lint based on my experience and opinion. +# It is very strict, but not extremely strict. +# Feel free to adopt and change it for your needs. + +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + + +# This file contains only configs which differ from defaults. +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 30 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 10.0 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 + + gocognit: + # Minimal code complexity to report + # Default: 30 (but we recommend 10-20) + min-complexity: 20 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + gomnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date` + # Default: [] + ignored-functions: + - os.Chmod + - os.Mkdir + - os.MkdirAll + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets + - prometheus.ExponentialBucketsRange + - prometheus.LinearBuckets + - strconv.FormatFloat + - strconv.FormatInt + - strconv.FormatUint + - strconv.ParseFloat + - strconv.ParseInt + - strconv.ParseUint + + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/google/uuid + reason: "see recommendation from dev-infra team: https://confluence.gtforge.com/x/gQI6Aw" + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: true + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [ funlen, gocognit, lll ] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + varcheck: + # Check usage of exported fields and variables. + # Default: false + exported-fields: false # default false # TODO: enable after fixing false positives + + +linters: + disable-all: true + enable: + ## enabled by default + - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases + - gosimple # Linter for Go source code that specializes in simplifying a code + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # Detects when assignments to existing variables are not used + - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code + - unused # Checks Go code for unused constants, variables, functions and types + ## disabled by default + # - asasalint # Check for pass []any as any in variadic func(...any) + - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers + - bidichk # Checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - contextcheck # check the function whether use a non-inherited context + - cyclop # checks function and package cyclomatic complexity + - dupl # Tool for code clone detection + - durationcheck # check for two durations multiplied together + - errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error. + - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. + - execinquery # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds + - exhaustive # check exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - forbidigo # Forbids identifiers + - funlen # Tool for detection of long functions + # - gochecknoglobals # check that no global variables exist + - gochecknoinits # Checks that no init functions are present in Go code + - gocognit # Computes and checks the cognitive complexity of functions + - goconst # Finds repeated strings that could be replaced by a constant + - gocritic # Provides diagnostics that check for bugs, performance and style issues. + - gocyclo # Computes and checks the cyclomatic complexity of functions + - godot # Check if comments end in a period + - goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. + - gomnd # An analyzer to detect magic numbers. + - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. + - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. + - goprintffuncname # Checks that printf-like functions are named with f at the end + - gosec # Inspects source code for security problems + - lll # Reports long lines + - makezero # Finds slice declarations with non-zero initial length + # - nakedret # Finds naked returns in functions greater than a specified function length + - nestif # Reports deeply nested if statements + - nilerr # Finds the code that returns nil even if it checks that the error is not nil. + - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. + # - noctx # noctx finds sending http request without context.Context + - nolintlint # Reports ill-formed or insufficient nolint directives + # - nonamedreturns # Reports all named returns + - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. + - predeclared # find code that shadows one of Go's predeclared identifiers + - promlinter # Check Prometheus metrics naming via promlint + - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. + - rowserrcheck # checks whether Err of rows is checked successfully + - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. + - stylecheck # Stylecheck is a replacement for golint + - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 + - testpackage # linter that makes you use a separate _test package + - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # Remove unnecessary type conversions + - unparam # Reports unused function parameters + - wastedassign # wastedassign finds wasted assignment statements. + - whitespace # Tool for detection of leading and trailing whitespace + ## you may want to enable + #- decorder # check declaration order and count of types, constants, variables and functions + #- exhaustruct # Checks if all structure fields are initialized + #- goheader # Checks is file header matches to pattern + #- ireturn # Accept Interfaces, Return Concrete Types + #- prealloc # [premature optimization, but can be used in some cases] Finds slice declarations that could potentially be preallocated + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- wrapcheck # Checks that errors returned from external packages are wrapped + ## disabled + #- containedctx # containedctx is a linter that detects struct contained context.Context field + #- depguard # [replaced by gomodguard] Go linter that checks if package imports are in a list of acceptable packages + #- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- gci # Gci controls golang package import order and makes it always deterministic. + #- godox # Tool for detection of FIXME, TODO and other comment keywords + #- goerr113 # [too strict] Golang linter to check the errors handling expressions + #- gofmt # [replaced by goimports] Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification + #- gofumpt # [replaced by goimports, gofumports is not available yet] Gofumpt checks whether code was gofumpt-ed. + #- grouper # An analyzer to analyze expression groups. + #- ifshort # Checks that your code uses short syntax for if-statements whenever possible + #- importas # Enforces consistent import aliases + #- maintidx # maintidx measures the maintainability index of each function. + #- misspell # [useless] Finds commonly misspelled English words in comments + #- nlreturn # [too strict and mostly code is not more readable] nlreturn checks for a new line before return and branch statements to increase code clarity + #- nosnakecase # Detects snake case of variable naming and function name. # TODO: maybe enable after https://github.com/sivchari/nosnakecase/issues/14 + #- paralleltest # [too many false positives] paralleltest detects missing usage of t.Parallel() method in your Go test + #- tagliatelle # Checks the struct tags. + #- thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers + #- wsl # [too strict and mostly code is not more readable] Whitespace Linter - Forces you to use empty lines! + ## deprecated + #- exhaustivestruct # [deprecated, replaced by exhaustruct] Checks if all struct's fields are initialized + #- golint # [deprecated, replaced by revive] Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes + #- interfacer # [deprecated] Linter that suggests narrower interface types + #- maligned # [deprecated, replaced by govet fieldalignment] Tool to detect Go structs that would take less memory if their fields were sorted + #- scopelint # [deprecated, replaced by exportloopref] Scopelint checks for unpinned variables in go programs + + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 50 + + exclude-rules: + - source: "^//\\s*go:generate\\s" + linters: [ lll ] + - source: "(noinspection|TODO)" + linters: [ godot ] + - source: "//noinspection" + linters: [ gocritic ] + - source: "^\\s+if _, ok := err\\.\\([^.]+\\.InternalError\\); ok {" + linters: [ errorlint ] + - path: "_test\\.go" + linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck diff --git a/vendor/github.com/sashabaranov/go-openai/CONTRIBUTING.md b/vendor/github.com/sashabaranov/go-openai/CONTRIBUTING.md new file mode 100644 index 00000000..4dd18404 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/CONTRIBUTING.md @@ -0,0 +1,88 @@ +# Contributing Guidelines + +## Overview +Thank you for your interest in contributing to the "Go OpenAI" project! By following this guideline, we hope to ensure that your contributions are made smoothly and efficiently. The Go OpenAI project is licensed under the [Apache 2.0 License](https://github.com/sashabaranov/go-openai/blob/master/LICENSE), and we welcome contributions through GitHub pull requests. + +## Reporting Bugs +If you discover a bug, first check the [GitHub Issues page](https://github.com/sashabaranov/go-openai/issues) to see if the issue has already been reported. If you're reporting a new issue, please use the "Bug report" template and provide detailed information about the problem, including steps to reproduce it. + +## Suggesting Features +If you want to suggest a new feature or improvement, first check the [GitHub Issues page](https://github.com/sashabaranov/go-openai/issues) to ensure a similar suggestion hasn't already been made. Use the "Feature request" template to provide a detailed description of your suggestion. + +## Reporting Vulnerabilities +If you identify a security concern, please use the "Report a security vulnerability" template on the [GitHub Issues page](https://github.com/sashabaranov/go-openai/issues) to share the details. This report will only be viewable to repository maintainers. You will be credited if the advisory is published. + +## Questions for Users +If you have questions, please utilize [StackOverflow](https://stackoverflow.com/) or the [GitHub Discussions page](https://github.com/sashabaranov/go-openai/discussions). + +## Contributing Code +There might already be a similar pull requests submitted! Please search for [pull requests](https://github.com/sashabaranov/go-openai/pulls) before creating one. + +### Requirements for Merging a Pull Request + +The requirements to accept a pull request are as follows: + +- Features not provided by the OpenAI API will not be accepted. +- The functionality of the feature must match that of the official OpenAI API. +- All pull requests should be written in Go according to common conventions, formatted with `goimports`, and free of warnings from tools like `golangci-lint`. +- Include tests and ensure all tests pass. +- Maintain test coverage without any reduction. +- All pull requests require approval from at least one Go OpenAI maintainer. + +**Note:** +The merging method for pull requests in this repository is squash merge. + +### Creating a Pull Request +- Fork the repository. +- Create a new branch and commit your changes. +- Push that branch to GitHub. +- Start a new Pull Request on GitHub. (Please use the pull request template to provide detailed information.) + +**Note:** +If your changes introduce breaking changes, please prefix your pull request title with "[BREAKING_CHANGES]". + +### Code Style +In this project, we adhere to the standard coding style of Go. Your code should maintain consistency with the rest of the codebase. To achieve this, please format your code using tools like `goimports` and resolve any syntax or style issues with `golangci-lint`. + +**Run goimports:** +``` +go install golang.org/x/tools/cmd/goimports@latest +``` + +``` +goimports -w . +``` + +**Run golangci-lint:** +``` +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest +``` + +``` +golangci-lint run --out-format=github-actions +``` + +### Unit Test +Please create or update tests relevant to your changes. Ensure all tests run successfully to verify that your modifications do not adversely affect other functionalities. + +**Run test:** +``` +go test -v ./... +``` + +### Integration Test +Integration tests are requested against the production version of the OpenAI API. These tests will verify that the library is properly coded against the actual behavior of the API, and will fail upon any incompatible change in the API. + +**Notes:** +These tests send real network traffic to the OpenAI API and may reach rate limits. Temporary network problems may also cause the test to fail. + +**Run integration test:** +``` +OPENAI_TOKEN=XXX go test -v -tags=integration ./api_integration_test.go +``` + +If the `OPENAI_TOKEN` environment variable is not available, integration tests will be skipped. + +--- + +We wholeheartedly welcome your active participation. Let's build an amazing project together! diff --git a/vendor/github.com/sashabaranov/go-openai/LICENSE b/vendor/github.com/sashabaranov/go-openai/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/github.com/sashabaranov/go-openai/README.md b/vendor/github.com/sashabaranov/go-openai/README.md new file mode 100644 index 00000000..57d1d35b --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/README.md @@ -0,0 +1,853 @@ +# Go OpenAI +[![Go Reference](https://pkg.go.dev/badge/github.com/sashabaranov/go-openai.svg)](https://pkg.go.dev/github.com/sashabaranov/go-openai) +[![Go Report Card](https://goreportcard.com/badge/github.com/sashabaranov/go-openai)](https://goreportcard.com/report/github.com/sashabaranov/go-openai) +[![codecov](https://codecov.io/gh/sashabaranov/go-openai/branch/master/graph/badge.svg?token=bCbIfHLIsW)](https://codecov.io/gh/sashabaranov/go-openai) + +This library provides unofficial Go clients for [OpenAI API](https://platform.openai.com/). We support: + +* ChatGPT 4o, o1 +* GPT-3, GPT-4 +* DALL·E 2, DALL·E 3 +* Whisper + +## Installation + +``` +go get github.com/sashabaranov/go-openai +``` +Currently, go-openai requires Go version 1.18 or greater. + + +## Usage + +### ChatGPT example usage: + +```go +package main + +import ( + "context" + "fmt" + openai "github.com/sashabaranov/go-openai" +) + +func main() { + client := openai.NewClient("your token") + resp, err := client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: openai.GPT3Dot5Turbo, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: "Hello!", + }, + }, + }, + ) + + if err != nil { + fmt.Printf("ChatCompletion error: %v\n", err) + return + } + + fmt.Println(resp.Choices[0].Message.Content) +} + +``` + +### Getting an OpenAI API Key: + +1. Visit the OpenAI website at [https://platform.openai.com/account/api-keys](https://platform.openai.com/account/api-keys). +2. If you don't have an account, click on "Sign Up" to create one. If you do, click "Log In". +3. Once logged in, navigate to your API key management page. +4. Click on "Create new secret key". +5. Enter a name for your new key, then click "Create secret key". +6. Your new API key will be displayed. Use this key to interact with the OpenAI API. + +**Note:** Your API key is sensitive information. Do not share it with anyone. + +### Other examples: + +
+ChatGPT streaming completion + +```go +package main + +import ( + "context" + "errors" + "fmt" + "io" + openai "github.com/sashabaranov/go-openai" +) + +func main() { + c := openai.NewClient("your token") + ctx := context.Background() + + req := openai.ChatCompletionRequest{ + Model: openai.GPT3Dot5Turbo, + MaxTokens: 20, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: "Lorem ipsum", + }, + }, + Stream: true, + } + stream, err := c.CreateChatCompletionStream(ctx, req) + if err != nil { + fmt.Printf("ChatCompletionStream error: %v\n", err) + return + } + defer stream.Close() + + fmt.Printf("Stream response: ") + for { + response, err := stream.Recv() + if errors.Is(err, io.EOF) { + fmt.Println("\nStream finished") + return + } + + if err != nil { + fmt.Printf("\nStream error: %v\n", err) + return + } + + fmt.Printf(response.Choices[0].Delta.Content) + } +} +``` +
+ +
+GPT-3 completion + +```go +package main + +import ( + "context" + "fmt" + openai "github.com/sashabaranov/go-openai" +) + +func main() { + c := openai.NewClient("your token") + ctx := context.Background() + + req := openai.CompletionRequest{ + Model: openai.GPT3Babbage002, + MaxTokens: 5, + Prompt: "Lorem ipsum", + } + resp, err := c.CreateCompletion(ctx, req) + if err != nil { + fmt.Printf("Completion error: %v\n", err) + return + } + fmt.Println(resp.Choices[0].Text) +} +``` +
+ +
+GPT-3 streaming completion + +```go +package main + +import ( + "errors" + "context" + "fmt" + "io" + openai "github.com/sashabaranov/go-openai" +) + +func main() { + c := openai.NewClient("your token") + ctx := context.Background() + + req := openai.CompletionRequest{ + Model: openai.GPT3Babbage002, + MaxTokens: 5, + Prompt: "Lorem ipsum", + Stream: true, + } + stream, err := c.CreateCompletionStream(ctx, req) + if err != nil { + fmt.Printf("CompletionStream error: %v\n", err) + return + } + defer stream.Close() + + for { + response, err := stream.Recv() + if errors.Is(err, io.EOF) { + fmt.Println("Stream finished") + return + } + + if err != nil { + fmt.Printf("Stream error: %v\n", err) + return + } + + + fmt.Printf("Stream response: %v\n", response) + } +} +``` +
+ +
+Audio Speech-To-Text + +```go +package main + +import ( + "context" + "fmt" + + openai "github.com/sashabaranov/go-openai" +) + +func main() { + c := openai.NewClient("your token") + ctx := context.Background() + + req := openai.AudioRequest{ + Model: openai.Whisper1, + FilePath: "recording.mp3", + } + resp, err := c.CreateTranscription(ctx, req) + if err != nil { + fmt.Printf("Transcription error: %v\n", err) + return + } + fmt.Println(resp.Text) +} +``` +
+ +
+Audio Captions + +```go +package main + +import ( + "context" + "fmt" + "os" + + openai "github.com/sashabaranov/go-openai" +) + +func main() { + c := openai.NewClient(os.Getenv("OPENAI_KEY")) + + req := openai.AudioRequest{ + Model: openai.Whisper1, + FilePath: os.Args[1], + Format: openai.AudioResponseFormatSRT, + } + resp, err := c.CreateTranscription(context.Background(), req) + if err != nil { + fmt.Printf("Transcription error: %v\n", err) + return + } + f, err := os.Create(os.Args[1] + ".srt") + if err != nil { + fmt.Printf("Could not open file: %v\n", err) + return + } + defer f.Close() + if _, err := f.WriteString(resp.Text); err != nil { + fmt.Printf("Error writing to file: %v\n", err) + return + } +} +``` +
+ +
+DALL-E 2 image generation + +```go +package main + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + openai "github.com/sashabaranov/go-openai" + "image/png" + "os" +) + +func main() { + c := openai.NewClient("your token") + ctx := context.Background() + + // Sample image by link + reqUrl := openai.ImageRequest{ + Prompt: "Parrot on a skateboard performs a trick, cartoon style, natural light, high detail", + Size: openai.CreateImageSize256x256, + ResponseFormat: openai.CreateImageResponseFormatURL, + N: 1, + } + + respUrl, err := c.CreateImage(ctx, reqUrl) + if err != nil { + fmt.Printf("Image creation error: %v\n", err) + return + } + fmt.Println(respUrl.Data[0].URL) + + // Example image as base64 + reqBase64 := openai.ImageRequest{ + Prompt: "Portrait of a humanoid parrot in a classic costume, high detail, realistic light, unreal engine", + Size: openai.CreateImageSize256x256, + ResponseFormat: openai.CreateImageResponseFormatB64JSON, + N: 1, + } + + respBase64, err := c.CreateImage(ctx, reqBase64) + if err != nil { + fmt.Printf("Image creation error: %v\n", err) + return + } + + imgBytes, err := base64.StdEncoding.DecodeString(respBase64.Data[0].B64JSON) + if err != nil { + fmt.Printf("Base64 decode error: %v\n", err) + return + } + + r := bytes.NewReader(imgBytes) + imgData, err := png.Decode(r) + if err != nil { + fmt.Printf("PNG decode error: %v\n", err) + return + } + + file, err := os.Create("example.png") + if err != nil { + fmt.Printf("File creation error: %v\n", err) + return + } + defer file.Close() + + if err := png.Encode(file, imgData); err != nil { + fmt.Printf("PNG encode error: %v\n", err) + return + } + + fmt.Println("The image was saved as example.png") +} + +``` +
+ +
+Configuring proxy + +```go +config := openai.DefaultConfig("token") +proxyUrl, err := url.Parse("http://localhost:{port}") +if err != nil { + panic(err) +} +transport := &http.Transport{ + Proxy: http.ProxyURL(proxyUrl), +} +config.HTTPClient = &http.Client{ + Transport: transport, +} + +c := openai.NewClientWithConfig(config) +``` + +See also: https://pkg.go.dev/github.com/sashabaranov/go-openai#ClientConfig +
+ +
+ChatGPT support context + +```go +package main + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + "github.com/sashabaranov/go-openai" +) + +func main() { + client := openai.NewClient("your token") + messages := make([]openai.ChatCompletionMessage, 0) + reader := bufio.NewReader(os.Stdin) + fmt.Println("Conversation") + fmt.Println("---------------------") + + for { + fmt.Print("-> ") + text, _ := reader.ReadString('\n') + // convert CRLF to LF + text = strings.Replace(text, "\n", "", -1) + messages = append(messages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: text, + }) + + resp, err := client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: openai.GPT3Dot5Turbo, + Messages: messages, + }, + ) + + if err != nil { + fmt.Printf("ChatCompletion error: %v\n", err) + continue + } + + content := resp.Choices[0].Message.Content + messages = append(messages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleAssistant, + Content: content, + }) + fmt.Println(content) + } +} +``` +
+ +
+Azure OpenAI ChatGPT + +```go +package main + +import ( + "context" + "fmt" + + openai "github.com/sashabaranov/go-openai" +) + +func main() { + config := openai.DefaultAzureConfig("your Azure OpenAI Key", "https://your Azure OpenAI Endpoint") + // If you use a deployment name different from the model name, you can customize the AzureModelMapperFunc function + // config.AzureModelMapperFunc = func(model string) string { + // azureModelMapping := map[string]string{ + // "gpt-3.5-turbo": "your gpt-3.5-turbo deployment name", + // } + // return azureModelMapping[model] + // } + + client := openai.NewClientWithConfig(config) + resp, err := client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: openai.GPT3Dot5Turbo, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: "Hello Azure OpenAI!", + }, + }, + }, + ) + if err != nil { + fmt.Printf("ChatCompletion error: %v\n", err) + return + } + + fmt.Println(resp.Choices[0].Message.Content) +} + +``` +
+ +
+Embedding Semantic Similarity + +```go +package main + +import ( + "context" + "log" + openai "github.com/sashabaranov/go-openai" + +) + +func main() { + client := openai.NewClient("your-token") + + // Create an EmbeddingRequest for the user query + queryReq := openai.EmbeddingRequest{ + Input: []string{"How many chucks would a woodchuck chuck"}, + Model: openai.AdaEmbeddingV2, + } + + // Create an embedding for the user query + queryResponse, err := client.CreateEmbeddings(context.Background(), queryReq) + if err != nil { + log.Fatal("Error creating query embedding:", err) + } + + // Create an EmbeddingRequest for the target text + targetReq := openai.EmbeddingRequest{ + Input: []string{"How many chucks would a woodchuck chuck if the woodchuck could chuck wood"}, + Model: openai.AdaEmbeddingV2, + } + + // Create an embedding for the target text + targetResponse, err := client.CreateEmbeddings(context.Background(), targetReq) + if err != nil { + log.Fatal("Error creating target embedding:", err) + } + + // Now that we have the embeddings for the user query and the target text, we + // can calculate their similarity. + queryEmbedding := queryResponse.Data[0] + targetEmbedding := targetResponse.Data[0] + + similarity, err := queryEmbedding.DotProduct(&targetEmbedding) + if err != nil { + log.Fatal("Error calculating dot product:", err) + } + + log.Printf("The similarity score between the query and the target is %f", similarity) +} + +``` +
+ +
+Azure OpenAI Embeddings + +```go +package main + +import ( + "context" + "fmt" + + openai "github.com/sashabaranov/go-openai" +) + +func main() { + + config := openai.DefaultAzureConfig("your Azure OpenAI Key", "https://your Azure OpenAI Endpoint") + config.APIVersion = "2023-05-15" // optional update to latest API version + + //If you use a deployment name different from the model name, you can customize the AzureModelMapperFunc function + //config.AzureModelMapperFunc = func(model string) string { + // azureModelMapping := map[string]string{ + // "gpt-3.5-turbo":"your gpt-3.5-turbo deployment name", + // } + // return azureModelMapping[model] + //} + + input := "Text to vectorize" + + client := openai.NewClientWithConfig(config) + resp, err := client.CreateEmbeddings( + context.Background(), + openai.EmbeddingRequest{ + Input: []string{input}, + Model: openai.AdaEmbeddingV2, + }) + + if err != nil { + fmt.Printf("CreateEmbeddings error: %v\n", err) + return + } + + vectors := resp.Data[0].Embedding // []float32 with 1536 dimensions + + fmt.Println(vectors[:10], "...", vectors[len(vectors)-10:]) +} +``` +
+ +
+JSON Schema for function calling + +It is now possible for chat completion to choose to call a function for more information ([see developer docs here](https://platform.openai.com/docs/guides/gpt/function-calling)). + +In order to describe the type of functions that can be called, a JSON schema must be provided. Many JSON schema libraries exist and are more advanced than what we can offer in this library, however we have included a simple `jsonschema` package for those who want to use this feature without formatting their own JSON schema payload. + +The developer documents give this JSON schema definition as an example: + +```json +{ + "name":"get_current_weather", + "description":"Get the current weather in a given location", + "parameters":{ + "type":"object", + "properties":{ + "location":{ + "type":"string", + "description":"The city and state, e.g. San Francisco, CA" + }, + "unit":{ + "type":"string", + "enum":[ + "celsius", + "fahrenheit" + ] + } + }, + "required":[ + "location" + ] + } +} +``` + +Using the `jsonschema` package, this schema could be created using structs as such: + +```go +FunctionDefinition{ + Name: "get_current_weather", + Parameters: jsonschema.Definition{ + Type: jsonschema.Object, + Properties: map[string]jsonschema.Definition{ + "location": { + Type: jsonschema.String, + Description: "The city and state, e.g. San Francisco, CA", + }, + "unit": { + Type: jsonschema.String, + Enum: []string{"celsius", "fahrenheit"}, + }, + }, + Required: []string{"location"}, + }, +} +``` + +The `Parameters` field of a `FunctionDefinition` can accept either of the above styles, or even a nested struct from another library (as long as it can be marshalled into JSON). +
+ +
+Error handling + +Open-AI maintains clear documentation on how to [handle API errors](https://platform.openai.com/docs/guides/error-codes/api-errors) + +example: +``` +e := &openai.APIError{} +if errors.As(err, &e) { + switch e.HTTPStatusCode { + case 401: + // invalid auth or key (do not retry) + case 429: + // rate limiting or engine overload (wait and retry) + case 500: + // openai server error (retry) + default: + // unhandled + } +} + +``` +
+ +
+Fine Tune Model + +```go +package main + +import ( + "context" + "fmt" + "github.com/sashabaranov/go-openai" +) + +func main() { + client := openai.NewClient("your token") + ctx := context.Background() + + // create a .jsonl file with your training data for conversational model + // {"prompt": "", "completion": ""} + // {"prompt": "", "completion": ""} + // {"prompt": "", "completion": ""} + + // chat models are trained using the following file format: + // {"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]} + // {"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]} + // {"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]} + + // you can use openai cli tool to validate the data + // For more info - https://platform.openai.com/docs/guides/fine-tuning + + file, err := client.CreateFile(ctx, openai.FileRequest{ + FilePath: "training_prepared.jsonl", + Purpose: "fine-tune", + }) + if err != nil { + fmt.Printf("Upload JSONL file error: %v\n", err) + return + } + + // create a fine tuning job + // Streams events until the job is done (this often takes minutes, but can take hours if there are many jobs in the queue or your dataset is large) + // use below get method to know the status of your model + fineTuningJob, err := client.CreateFineTuningJob(ctx, openai.FineTuningJobRequest{ + TrainingFile: file.ID, + Model: "davinci-002", // gpt-3.5-turbo-0613, babbage-002. + }) + if err != nil { + fmt.Printf("Creating new fine tune model error: %v\n", err) + return + } + + fineTuningJob, err = client.RetrieveFineTuningJob(ctx, fineTuningJob.ID) + if err != nil { + fmt.Printf("Getting fine tune model error: %v\n", err) + return + } + fmt.Println(fineTuningJob.FineTunedModel) + + // once the status of fineTuningJob is `succeeded`, you can use your fine tune model in Completion Request or Chat Completion Request + + // resp, err := client.CreateCompletion(ctx, openai.CompletionRequest{ + // Model: fineTuningJob.FineTunedModel, + // Prompt: "your prompt", + // }) + // if err != nil { + // fmt.Printf("Create completion error %v\n", err) + // return + // } + // + // fmt.Println(resp.Choices[0].Text) +} +``` +
+ +
+Structured Outputs + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/sashabaranov/go-openai" + "github.com/sashabaranov/go-openai/jsonschema" +) + +func main() { + client := openai.NewClient("your token") + ctx := context.Background() + + type Result struct { + Steps []struct { + Explanation string `json:"explanation"` + Output string `json:"output"` + } `json:"steps"` + FinalAnswer string `json:"final_answer"` + } + var result Result + schema, err := jsonschema.GenerateSchemaForType(result) + if err != nil { + log.Fatalf("GenerateSchemaForType error: %v", err) + } + resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ + Model: openai.GPT4oMini, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: "You are a helpful math tutor. Guide the user through the solution step by step.", + }, + { + Role: openai.ChatMessageRoleUser, + Content: "how can I solve 8x + 7 = -23", + }, + }, + ResponseFormat: &openai.ChatCompletionResponseFormat{ + Type: openai.ChatCompletionResponseFormatTypeJSONSchema, + JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{ + Name: "math_reasoning", + Schema: schema, + Strict: true, + }, + }, + }) + if err != nil { + log.Fatalf("CreateChatCompletion error: %v", err) + } + err = schema.Unmarshal(resp.Choices[0].Message.Content, &result) + if err != nil { + log.Fatalf("Unmarshal schema error: %v", err) + } + fmt.Println(result) +} +``` +
+See the `examples/` folder for more. + +## Frequently Asked Questions + +### Why don't we get the same answer when specifying a temperature field of 0 and asking the same question? + +Even when specifying a temperature field of 0, it doesn't guarantee that you'll always get the same response. Several factors come into play. + +1. Go OpenAI Behavior: When you specify a temperature field of 0 in Go OpenAI, the omitempty tag causes that field to be removed from the request. Consequently, the OpenAI API applies the default value of 1. +2. Token Count for Input/Output: If there's a large number of tokens in the input and output, setting the temperature to 0 can still result in non-deterministic behavior. In particular, when using around 32k tokens, the likelihood of non-deterministic behavior becomes highest even with a temperature of 0. + +Due to the factors mentioned above, different answers may be returned even for the same question. + +**Workarounds:** +1. As of November 2023, use [the new `seed` parameter](https://platform.openai.com/docs/guides/text-generation/reproducible-outputs) in conjunction with the `system_fingerprint` response field, alongside Temperature management. +2. Try using `math.SmallestNonzeroFloat32`: By specifying `math.SmallestNonzeroFloat32` in the temperature field instead of 0, you can mimic the behavior of setting it to 0. +3. Limiting Token Count: By limiting the number of tokens in the input and output and especially avoiding large requests close to 32k tokens, you can reduce the risk of non-deterministic behavior. + +By adopting these strategies, you can expect more consistent results. + +**Related Issues:** +[omitempty option of request struct will generate incorrect request when parameter is 0.](https://github.com/sashabaranov/go-openai/issues/9) + +### Does Go OpenAI provide a method to count tokens? + +No, Go OpenAI does not offer a feature to count tokens, and there are no plans to provide such a feature in the future. However, if there's a way to implement a token counting feature with zero dependencies, it might be possible to merge that feature into Go OpenAI. Otherwise, it would be more appropriate to implement it in a dedicated library or repository. + +For counting tokens, you might find the following links helpful: +- [Counting Tokens For Chat API Calls](https://github.com/pkoukk/tiktoken-go#counting-tokens-for-chat-api-calls) +- [How to count tokens with tiktoken](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb) + +**Related Issues:** +[Is it possible to join the implementation of GPT3 Tokenizer](https://github.com/sashabaranov/go-openai/issues/62) + +## Contributing + +By following [Contributing Guidelines](https://github.com/sashabaranov/go-openai/blob/master/CONTRIBUTING.md), we hope to ensure that your contributions are made smoothly and efficiently. + +## Thank you + +We want to take a moment to express our deepest gratitude to the [contributors](https://github.com/sashabaranov/go-openai/graphs/contributors) and sponsors of this project: +- [Carson Kahn](https://carsonkahn.com) of [Spindle AI](https://spindleai.com) + +To all of you: thank you. You've helped us achieve more than we ever imagined possible. Can't wait to see where we go next, together! diff --git a/vendor/github.com/sashabaranov/go-openai/assistant.go b/vendor/github.com/sashabaranov/go-openai/assistant.go new file mode 100644 index 00000000..8aab5bcf --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/assistant.go @@ -0,0 +1,325 @@ +package openai + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +const ( + assistantsSuffix = "/assistants" + assistantsFilesSuffix = "/files" +) + +type Assistant struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Model string `json:"model"` + Instructions *string `json:"instructions,omitempty"` + Tools []AssistantTool `json:"tools"` + ToolResources *AssistantToolResource `json:"tool_resources,omitempty"` + FileIDs []string `json:"file_ids,omitempty"` // Deprecated in v2 + Metadata map[string]any `json:"metadata,omitempty"` + Temperature *float32 `json:"temperature,omitempty"` + TopP *float32 `json:"top_p,omitempty"` + ResponseFormat any `json:"response_format,omitempty"` + + httpHeader +} + +type AssistantToolType string + +const ( + AssistantToolTypeCodeInterpreter AssistantToolType = "code_interpreter" + AssistantToolTypeRetrieval AssistantToolType = "retrieval" + AssistantToolTypeFunction AssistantToolType = "function" + AssistantToolTypeFileSearch AssistantToolType = "file_search" +) + +type AssistantTool struct { + Type AssistantToolType `json:"type"` + Function *FunctionDefinition `json:"function,omitempty"` +} + +type AssistantToolFileSearch struct { + VectorStoreIDs []string `json:"vector_store_ids"` +} + +type AssistantToolCodeInterpreter struct { + FileIDs []string `json:"file_ids"` +} + +type AssistantToolResource struct { + FileSearch *AssistantToolFileSearch `json:"file_search,omitempty"` + CodeInterpreter *AssistantToolCodeInterpreter `json:"code_interpreter,omitempty"` +} + +// AssistantRequest provides the assistant request parameters. +// When modifying the tools the API functions as the following: +// If Tools is undefined, no changes are made to the Assistant's tools. +// If Tools is empty slice it will effectively delete all of the Assistant's tools. +// If Tools is populated, it will replace all of the existing Assistant's tools with the provided tools. +type AssistantRequest struct { + Model string `json:"model"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Instructions *string `json:"instructions,omitempty"` + Tools []AssistantTool `json:"-"` + FileIDs []string `json:"file_ids,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` + ToolResources *AssistantToolResource `json:"tool_resources,omitempty"` + ResponseFormat any `json:"response_format,omitempty"` + Temperature *float32 `json:"temperature,omitempty"` + TopP *float32 `json:"top_p,omitempty"` +} + +// MarshalJSON provides a custom marshaller for the assistant request to handle the API use cases +// If Tools is nil, the field is omitted from the JSON. +// If Tools is an empty slice, it's included in the JSON as an empty array ([]). +// If Tools is populated, it's included in the JSON with the elements. +func (a AssistantRequest) MarshalJSON() ([]byte, error) { + type Alias AssistantRequest + assistantAlias := &struct { + Tools *[]AssistantTool `json:"tools,omitempty"` + *Alias + }{ + Alias: (*Alias)(&a), + } + + if a.Tools != nil { + assistantAlias.Tools = &a.Tools + } + + return json.Marshal(assistantAlias) +} + +// AssistantsList is a list of assistants. +type AssistantsList struct { + Assistants []Assistant `json:"data"` + LastID *string `json:"last_id"` + FirstID *string `json:"first_id"` + HasMore bool `json:"has_more"` + httpHeader +} + +type AssistantDeleteResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + + httpHeader +} + +type AssistantFile struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + AssistantID string `json:"assistant_id"` + + httpHeader +} + +type AssistantFileRequest struct { + FileID string `json:"file_id"` +} + +type AssistantFilesList struct { + AssistantFiles []AssistantFile `json:"data"` + + httpHeader +} + +// CreateAssistant creates a new assistant. +func (c *Client) CreateAssistant(ctx context.Context, request AssistantRequest) (response Assistant, err error) { + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(assistantsSuffix), withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveAssistant retrieves an assistant. +func (c *Client) RetrieveAssistant( + ctx context.Context, + assistantID string, +) (response Assistant, err error) { + urlSuffix := fmt.Sprintf("%s/%s", assistantsSuffix, assistantID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ModifyAssistant modifies an assistant. +func (c *Client) ModifyAssistant( + ctx context.Context, + assistantID string, + request AssistantRequest, +) (response Assistant, err error) { + urlSuffix := fmt.Sprintf("%s/%s", assistantsSuffix, assistantID) + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// DeleteAssistant deletes an assistant. +func (c *Client) DeleteAssistant( + ctx context.Context, + assistantID string, +) (response AssistantDeleteResponse, err error) { + urlSuffix := fmt.Sprintf("%s/%s", assistantsSuffix, assistantID) + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ListAssistants Lists the currently available assistants. +func (c *Client) ListAssistants( + ctx context.Context, + limit *int, + order *string, + after *string, + before *string, +) (response AssistantsList, err error) { + urlValues := url.Values{} + if limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *limit)) + } + if order != nil { + urlValues.Add("order", *order) + } + if after != nil { + urlValues.Add("after", *after) + } + if before != nil { + urlValues.Add("before", *before) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("%s%s", assistantsSuffix, encodedValues) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CreateAssistantFile creates a new assistant file. +func (c *Client) CreateAssistantFile( + ctx context.Context, + assistantID string, + request AssistantFileRequest, +) (response AssistantFile, err error) { + urlSuffix := fmt.Sprintf("%s/%s%s", assistantsSuffix, assistantID, assistantsFilesSuffix) + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveAssistantFile retrieves an assistant file. +func (c *Client) RetrieveAssistantFile( + ctx context.Context, + assistantID string, + fileID string, +) (response AssistantFile, err error) { + urlSuffix := fmt.Sprintf("%s/%s%s/%s", assistantsSuffix, assistantID, assistantsFilesSuffix, fileID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// DeleteAssistantFile deletes an existing file. +func (c *Client) DeleteAssistantFile( + ctx context.Context, + assistantID string, + fileID string, +) (err error) { + urlSuffix := fmt.Sprintf("%s/%s%s/%s", assistantsSuffix, assistantID, assistantsFilesSuffix, fileID) + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, nil) + return +} + +// ListAssistantFiles Lists the currently available files for an assistant. +func (c *Client) ListAssistantFiles( + ctx context.Context, + assistantID string, + limit *int, + order *string, + after *string, + before *string, +) (response AssistantFilesList, err error) { + urlValues := url.Values{} + if limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *limit)) + } + if order != nil { + urlValues.Add("order", *order) + } + if after != nil { + urlValues.Add("after", *after) + } + if before != nil { + urlValues.Add("before", *before) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("%s/%s%s%s", assistantsSuffix, assistantID, assistantsFilesSuffix, encodedValues) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/audio.go b/vendor/github.com/sashabaranov/go-openai/audio.go new file mode 100644 index 00000000..f321f93d --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/audio.go @@ -0,0 +1,234 @@ +package openai + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "os" + + utils "github.com/sashabaranov/go-openai/internal" +) + +// Whisper Defines the models provided by OpenAI to use when processing audio with OpenAI. +const ( + Whisper1 = "whisper-1" +) + +// Response formats; Whisper uses AudioResponseFormatJSON by default. +type AudioResponseFormat string + +const ( + AudioResponseFormatJSON AudioResponseFormat = "json" + AudioResponseFormatText AudioResponseFormat = "text" + AudioResponseFormatSRT AudioResponseFormat = "srt" + AudioResponseFormatVerboseJSON AudioResponseFormat = "verbose_json" + AudioResponseFormatVTT AudioResponseFormat = "vtt" +) + +type TranscriptionTimestampGranularity string + +const ( + TranscriptionTimestampGranularityWord TranscriptionTimestampGranularity = "word" + TranscriptionTimestampGranularitySegment TranscriptionTimestampGranularity = "segment" +) + +// AudioRequest represents a request structure for audio API. +type AudioRequest struct { + Model string + + // FilePath is either an existing file in your filesystem or a filename representing the contents of Reader. + FilePath string + + // Reader is an optional io.Reader when you do not want to use an existing file. + Reader io.Reader + + Prompt string + Temperature float32 + Language string // Only for transcription. + Format AudioResponseFormat + TimestampGranularities []TranscriptionTimestampGranularity // Only for transcription. +} + +// AudioResponse represents a response structure for audio API. +type AudioResponse struct { + Task string `json:"task"` + Language string `json:"language"` + Duration float64 `json:"duration"` + Segments []struct { + ID int `json:"id"` + Seek int `json:"seek"` + Start float64 `json:"start"` + End float64 `json:"end"` + Text string `json:"text"` + Tokens []int `json:"tokens"` + Temperature float64 `json:"temperature"` + AvgLogprob float64 `json:"avg_logprob"` + CompressionRatio float64 `json:"compression_ratio"` + NoSpeechProb float64 `json:"no_speech_prob"` + Transient bool `json:"transient"` + } `json:"segments"` + Words []struct { + Word string `json:"word"` + Start float64 `json:"start"` + End float64 `json:"end"` + } `json:"words"` + Text string `json:"text"` + + httpHeader +} + +type audioTextResponse struct { + Text string `json:"text"` + + httpHeader +} + +func (r *audioTextResponse) ToAudioResponse() AudioResponse { + return AudioResponse{ + Text: r.Text, + httpHeader: r.httpHeader, + } +} + +// CreateTranscription — API call to create a transcription. Returns transcribed text. +func (c *Client) CreateTranscription( + ctx context.Context, + request AudioRequest, +) (response AudioResponse, err error) { + return c.callAudioAPI(ctx, request, "transcriptions") +} + +// CreateTranslation — API call to translate audio into English. +func (c *Client) CreateTranslation( + ctx context.Context, + request AudioRequest, +) (response AudioResponse, err error) { + return c.callAudioAPI(ctx, request, "translations") +} + +// callAudioAPI — API call to an audio endpoint. +func (c *Client) callAudioAPI( + ctx context.Context, + request AudioRequest, + endpointSuffix string, +) (response AudioResponse, err error) { + var formBody bytes.Buffer + builder := c.createFormBuilder(&formBody) + + if err = audioMultipartForm(request, builder); err != nil { + return AudioResponse{}, err + } + + urlSuffix := fmt.Sprintf("/audio/%s", endpointSuffix) + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix, withModel(request.Model)), + withBody(&formBody), + withContentType(builder.FormDataContentType()), + ) + if err != nil { + return AudioResponse{}, err + } + + if request.HasJSONResponse() { + err = c.sendRequest(req, &response) + } else { + var textResponse audioTextResponse + err = c.sendRequest(req, &textResponse) + response = textResponse.ToAudioResponse() + } + if err != nil { + return AudioResponse{}, err + } + return +} + +// HasJSONResponse returns true if the response format is JSON. +func (r AudioRequest) HasJSONResponse() bool { + return r.Format == "" || r.Format == AudioResponseFormatJSON || r.Format == AudioResponseFormatVerboseJSON +} + +// audioMultipartForm creates a form with audio file contents and the name of the model to use for +// audio processing. +func audioMultipartForm(request AudioRequest, b utils.FormBuilder) error { + err := createFileField(request, b) + if err != nil { + return err + } + + err = b.WriteField("model", request.Model) + if err != nil { + return fmt.Errorf("writing model name: %w", err) + } + + // Create a form field for the prompt (if provided) + if request.Prompt != "" { + err = b.WriteField("prompt", request.Prompt) + if err != nil { + return fmt.Errorf("writing prompt: %w", err) + } + } + + // Create a form field for the format (if provided) + if request.Format != "" { + err = b.WriteField("response_format", string(request.Format)) + if err != nil { + return fmt.Errorf("writing format: %w", err) + } + } + + // Create a form field for the temperature (if provided) + if request.Temperature != 0 { + err = b.WriteField("temperature", fmt.Sprintf("%.2f", request.Temperature)) + if err != nil { + return fmt.Errorf("writing temperature: %w", err) + } + } + + // Create a form field for the language (if provided) + if request.Language != "" { + err = b.WriteField("language", request.Language) + if err != nil { + return fmt.Errorf("writing language: %w", err) + } + } + + if len(request.TimestampGranularities) > 0 { + for _, tg := range request.TimestampGranularities { + err = b.WriteField("timestamp_granularities[]", string(tg)) + if err != nil { + return fmt.Errorf("writing timestamp_granularities[]: %w", err) + } + } + } + + // Close the multipart writer + return b.Close() +} + +// createFileField creates the "file" form field from either an existing file or by using the reader. +func createFileField(request AudioRequest, b utils.FormBuilder) error { + if request.Reader != nil { + err := b.CreateFormFileReader("file", request.Reader, request.FilePath) + if err != nil { + return fmt.Errorf("creating form using reader: %w", err) + } + return nil + } + + f, err := os.Open(request.FilePath) + if err != nil { + return fmt.Errorf("opening audio file: %w", err) + } + defer f.Close() + + err = b.CreateFormFile("file", f) + if err != nil { + return fmt.Errorf("creating form file: %w", err) + } + + return nil +} diff --git a/vendor/github.com/sashabaranov/go-openai/batch.go b/vendor/github.com/sashabaranov/go-openai/batch.go new file mode 100644 index 00000000..3c1a9d0d --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/batch.go @@ -0,0 +1,271 @@ +package openai + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +const batchesSuffix = "/batches" + +type BatchEndpoint string + +const ( + BatchEndpointChatCompletions BatchEndpoint = "/v1/chat/completions" + BatchEndpointCompletions BatchEndpoint = "/v1/completions" + BatchEndpointEmbeddings BatchEndpoint = "/v1/embeddings" +) + +type BatchLineItem interface { + MarshalBatchLineItem() []byte +} + +type BatchChatCompletionRequest struct { + CustomID string `json:"custom_id"` + Body ChatCompletionRequest `json:"body"` + Method string `json:"method"` + URL BatchEndpoint `json:"url"` +} + +func (r BatchChatCompletionRequest) MarshalBatchLineItem() []byte { + marshal, _ := json.Marshal(r) + return marshal +} + +type BatchCompletionRequest struct { + CustomID string `json:"custom_id"` + Body CompletionRequest `json:"body"` + Method string `json:"method"` + URL BatchEndpoint `json:"url"` +} + +func (r BatchCompletionRequest) MarshalBatchLineItem() []byte { + marshal, _ := json.Marshal(r) + return marshal +} + +type BatchEmbeddingRequest struct { + CustomID string `json:"custom_id"` + Body EmbeddingRequest `json:"body"` + Method string `json:"method"` + URL BatchEndpoint `json:"url"` +} + +func (r BatchEmbeddingRequest) MarshalBatchLineItem() []byte { + marshal, _ := json.Marshal(r) + return marshal +} + +type Batch struct { + ID string `json:"id"` + Object string `json:"object"` + Endpoint BatchEndpoint `json:"endpoint"` + Errors *struct { + Object string `json:"object,omitempty"` + Data []struct { + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Param *string `json:"param,omitempty"` + Line *int `json:"line,omitempty"` + } `json:"data"` + } `json:"errors"` + InputFileID string `json:"input_file_id"` + CompletionWindow string `json:"completion_window"` + Status string `json:"status"` + OutputFileID *string `json:"output_file_id"` + ErrorFileID *string `json:"error_file_id"` + CreatedAt int `json:"created_at"` + InProgressAt *int `json:"in_progress_at"` + ExpiresAt *int `json:"expires_at"` + FinalizingAt *int `json:"finalizing_at"` + CompletedAt *int `json:"completed_at"` + FailedAt *int `json:"failed_at"` + ExpiredAt *int `json:"expired_at"` + CancellingAt *int `json:"cancelling_at"` + CancelledAt *int `json:"cancelled_at"` + RequestCounts BatchRequestCounts `json:"request_counts"` + Metadata map[string]any `json:"metadata"` +} + +type BatchRequestCounts struct { + Total int `json:"total"` + Completed int `json:"completed"` + Failed int `json:"failed"` +} + +type CreateBatchRequest struct { + InputFileID string `json:"input_file_id"` + Endpoint BatchEndpoint `json:"endpoint"` + CompletionWindow string `json:"completion_window"` + Metadata map[string]any `json:"metadata"` +} + +type BatchResponse struct { + httpHeader + Batch +} + +// CreateBatch — API call to Create batch. +func (c *Client) CreateBatch( + ctx context.Context, + request CreateBatchRequest, +) (response BatchResponse, err error) { + if request.CompletionWindow == "" { + request.CompletionWindow = "24h" + } + + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(batchesSuffix), withBody(request)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +type UploadBatchFileRequest struct { + FileName string + Lines []BatchLineItem +} + +func (r *UploadBatchFileRequest) MarshalJSONL() []byte { + buff := bytes.Buffer{} + for i, line := range r.Lines { + if i != 0 { + buff.Write([]byte("\n")) + } + buff.Write(line.MarshalBatchLineItem()) + } + return buff.Bytes() +} + +func (r *UploadBatchFileRequest) AddChatCompletion(customerID string, body ChatCompletionRequest) { + r.Lines = append(r.Lines, BatchChatCompletionRequest{ + CustomID: customerID, + Body: body, + Method: "POST", + URL: BatchEndpointChatCompletions, + }) +} + +func (r *UploadBatchFileRequest) AddCompletion(customerID string, body CompletionRequest) { + r.Lines = append(r.Lines, BatchCompletionRequest{ + CustomID: customerID, + Body: body, + Method: "POST", + URL: BatchEndpointCompletions, + }) +} + +func (r *UploadBatchFileRequest) AddEmbedding(customerID string, body EmbeddingRequest) { + r.Lines = append(r.Lines, BatchEmbeddingRequest{ + CustomID: customerID, + Body: body, + Method: "POST", + URL: BatchEndpointEmbeddings, + }) +} + +// UploadBatchFile — upload batch file. +func (c *Client) UploadBatchFile(ctx context.Context, request UploadBatchFileRequest) (File, error) { + if request.FileName == "" { + request.FileName = "@batchinput.jsonl" + } + return c.CreateFileBytes(ctx, FileBytesRequest{ + Name: request.FileName, + Bytes: request.MarshalJSONL(), + Purpose: PurposeBatch, + }) +} + +type CreateBatchWithUploadFileRequest struct { + Endpoint BatchEndpoint `json:"endpoint"` + CompletionWindow string `json:"completion_window"` + Metadata map[string]any `json:"metadata"` + UploadBatchFileRequest +} + +// CreateBatchWithUploadFile — API call to Create batch with upload file. +func (c *Client) CreateBatchWithUploadFile( + ctx context.Context, + request CreateBatchWithUploadFileRequest, +) (response BatchResponse, err error) { + var file File + file, err = c.UploadBatchFile(ctx, UploadBatchFileRequest{ + FileName: request.FileName, + Lines: request.Lines, + }) + if err != nil { + return + } + return c.CreateBatch(ctx, CreateBatchRequest{ + InputFileID: file.ID, + Endpoint: request.Endpoint, + CompletionWindow: request.CompletionWindow, + Metadata: request.Metadata, + }) +} + +// RetrieveBatch — API call to Retrieve batch. +func (c *Client) RetrieveBatch( + ctx context.Context, + batchID string, +) (response BatchResponse, err error) { + urlSuffix := fmt.Sprintf("%s/%s", batchesSuffix, batchID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + err = c.sendRequest(req, &response) + return +} + +// CancelBatch — API call to Cancel batch. +func (c *Client) CancelBatch( + ctx context.Context, + batchID string, +) (response BatchResponse, err error) { + urlSuffix := fmt.Sprintf("%s/%s/cancel", batchesSuffix, batchID) + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix)) + if err != nil { + return + } + err = c.sendRequest(req, &response) + return +} + +type ListBatchResponse struct { + httpHeader + Object string `json:"object"` + Data []Batch `json:"data"` + FirstID string `json:"first_id"` + LastID string `json:"last_id"` + HasMore bool `json:"has_more"` +} + +// ListBatch API call to List batch. +func (c *Client) ListBatch(ctx context.Context, after *string, limit *int) (response ListBatchResponse, err error) { + urlValues := url.Values{} + if limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *limit)) + } + if after != nil { + urlValues.Add("after", *after) + } + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("%s%s", batchesSuffix, encodedValues) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/chat.go b/vendor/github.com/sashabaranov/go-openai/chat.go new file mode 100644 index 00000000..2b13f8dd --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/chat.go @@ -0,0 +1,409 @@ +package openai + +import ( + "context" + "encoding/json" + "errors" + "net/http" +) + +// Chat message role defined by the OpenAI API. +const ( + ChatMessageRoleSystem = "system" + ChatMessageRoleUser = "user" + ChatMessageRoleAssistant = "assistant" + ChatMessageRoleFunction = "function" + ChatMessageRoleTool = "tool" +) + +const chatCompletionsSuffix = "/chat/completions" + +var ( + ErrChatCompletionInvalidModel = errors.New("this model is not supported with this method, please use CreateCompletion client method instead") //nolint:lll + ErrChatCompletionStreamNotSupported = errors.New("streaming is not supported with this method, please use CreateChatCompletionStream") //nolint:lll + ErrContentFieldsMisused = errors.New("can't use both Content and MultiContent properties simultaneously") +) + +type Hate struct { + Filtered bool `json:"filtered"` + Severity string `json:"severity,omitempty"` +} +type SelfHarm struct { + Filtered bool `json:"filtered"` + Severity string `json:"severity,omitempty"` +} +type Sexual struct { + Filtered bool `json:"filtered"` + Severity string `json:"severity,omitempty"` +} +type Violence struct { + Filtered bool `json:"filtered"` + Severity string `json:"severity,omitempty"` +} + +type JailBreak struct { + Filtered bool `json:"filtered"` + Detected bool `json:"detected"` +} + +type Profanity struct { + Filtered bool `json:"filtered"` + Detected bool `json:"detected"` +} + +type ContentFilterResults struct { + Hate Hate `json:"hate,omitempty"` + SelfHarm SelfHarm `json:"self_harm,omitempty"` + Sexual Sexual `json:"sexual,omitempty"` + Violence Violence `json:"violence,omitempty"` + JailBreak JailBreak `json:"jailbreak,omitempty"` + Profanity Profanity `json:"profanity,omitempty"` +} + +type PromptAnnotation struct { + PromptIndex int `json:"prompt_index,omitempty"` + ContentFilterResults ContentFilterResults `json:"content_filter_results,omitempty"` +} + +type ImageURLDetail string + +const ( + ImageURLDetailHigh ImageURLDetail = "high" + ImageURLDetailLow ImageURLDetail = "low" + ImageURLDetailAuto ImageURLDetail = "auto" +) + +type ChatMessageImageURL struct { + URL string `json:"url,omitempty"` + Detail ImageURLDetail `json:"detail,omitempty"` +} + +type ChatMessagePartType string + +const ( + ChatMessagePartTypeText ChatMessagePartType = "text" + ChatMessagePartTypeImageURL ChatMessagePartType = "image_url" +) + +type ChatMessagePart struct { + Type ChatMessagePartType `json:"type,omitempty"` + Text string `json:"text,omitempty"` + ImageURL *ChatMessageImageURL `json:"image_url,omitempty"` +} + +type ChatCompletionMessage struct { + Role string `json:"role"` + Content string `json:"content"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart + + // This property isn't in the official documentation, but it's in + // the documentation for the official library for python: + // - https://github.com/openai/openai-python/blob/main/chatml.md + // - https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + Name string `json:"name,omitempty"` + + FunctionCall *FunctionCall `json:"function_call,omitempty"` + + // For Role=assistant prompts this may be set to the tool calls generated by the model, such as function calls. + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + + // For Role=tool prompts this should be set to the ID given in the assistant's prior request to call a tool. + ToolCallID string `json:"tool_call_id,omitempty"` +} + +func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) { + if m.Content != "" && m.MultiContent != nil { + return nil, ErrContentFieldsMisused + } + if len(m.MultiContent) > 0 { + msg := struct { + Role string `json:"role"` + Content string `json:"-"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart `json:"content,omitempty"` + Name string `json:"name,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` + }(m) + return json.Marshal(msg) + } + + msg := struct { + Role string `json:"role"` + Content string `json:"content"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart `json:"-"` + Name string `json:"name,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` + }(m) + return json.Marshal(msg) +} + +func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error { + msg := struct { + Role string `json:"role"` + Content string `json:"content"` + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart + Name string `json:"name,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` + }{} + + if err := json.Unmarshal(bs, &msg); err == nil { + *m = ChatCompletionMessage(msg) + return nil + } + multiMsg := struct { + Role string `json:"role"` + Content string + Refusal string `json:"refusal,omitempty"` + MultiContent []ChatMessagePart `json:"content"` + Name string `json:"name,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` + }{} + if err := json.Unmarshal(bs, &multiMsg); err != nil { + return err + } + *m = ChatCompletionMessage(multiMsg) + return nil +} + +type ToolCall struct { + // Index is not nil only in chat completion chunk object + Index *int `json:"index,omitempty"` + ID string `json:"id"` + Type ToolType `json:"type"` + Function FunctionCall `json:"function"` +} + +type FunctionCall struct { + Name string `json:"name,omitempty"` + // call function with arguments in JSON format + Arguments string `json:"arguments,omitempty"` +} + +type ChatCompletionResponseFormatType string + +const ( + ChatCompletionResponseFormatTypeJSONObject ChatCompletionResponseFormatType = "json_object" + ChatCompletionResponseFormatTypeJSONSchema ChatCompletionResponseFormatType = "json_schema" + ChatCompletionResponseFormatTypeText ChatCompletionResponseFormatType = "text" +) + +type ChatCompletionResponseFormat struct { + Type ChatCompletionResponseFormatType `json:"type,omitempty"` + JSONSchema *ChatCompletionResponseFormatJSONSchema `json:"json_schema,omitempty"` +} + +type ChatCompletionResponseFormatJSONSchema struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Schema json.Marshaler `json:"schema"` + Strict bool `json:"strict"` +} + +// ChatCompletionRequest represents a request structure for chat completion API. +type ChatCompletionRequest struct { + Model string `json:"model"` + Messages []ChatCompletionMessage `json:"messages"` + // MaxTokens The maximum number of tokens that can be generated in the chat completion. + // This value can be used to control costs for text generated via API. + // This value is now deprecated in favor of max_completion_tokens, and is not compatible with o1 series models. + // refs: https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_tokens + MaxTokens int `json:"max_tokens,omitempty"` + // MaxCompletionTokens An upper bound for the number of tokens that can be generated for a completion, + // including visible output tokens and reasoning tokens https://platform.openai.com/docs/guides/reasoning + MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` + Temperature float32 `json:"temperature,omitempty"` + TopP float32 `json:"top_p,omitempty"` + N int `json:"n,omitempty"` + Stream bool `json:"stream,omitempty"` + Stop []string `json:"stop,omitempty"` + PresencePenalty float32 `json:"presence_penalty,omitempty"` + ResponseFormat *ChatCompletionResponseFormat `json:"response_format,omitempty"` + Seed *int `json:"seed,omitempty"` + FrequencyPenalty float32 `json:"frequency_penalty,omitempty"` + // LogitBias is must be a token id string (specified by their token ID in the tokenizer), not a word string. + // incorrect: `"logit_bias":{"You": 6}`, correct: `"logit_bias":{"1639": 6}` + // refs: https://platform.openai.com/docs/api-reference/chat/create#chat/create-logit_bias + LogitBias map[string]int `json:"logit_bias,omitempty"` + // LogProbs indicates whether to return log probabilities of the output tokens or not. + // If true, returns the log probabilities of each output token returned in the content of message. + // This option is currently not available on the gpt-4-vision-preview model. + LogProbs bool `json:"logprobs,omitempty"` + // TopLogProbs is an integer between 0 and 5 specifying the number of most likely tokens to return at each + // token position, each with an associated log probability. + // logprobs must be set to true if this parameter is used. + TopLogProbs int `json:"top_logprobs,omitempty"` + User string `json:"user,omitempty"` + // Deprecated: use Tools instead. + Functions []FunctionDefinition `json:"functions,omitempty"` + // Deprecated: use ToolChoice instead. + FunctionCall any `json:"function_call,omitempty"` + Tools []Tool `json:"tools,omitempty"` + // This can be either a string or an ToolChoice object. + ToolChoice any `json:"tool_choice,omitempty"` + // Options for streaming response. Only set this when you set stream: true. + StreamOptions *StreamOptions `json:"stream_options,omitempty"` + // Disable the default behavior of parallel tool calls by setting it: false. + ParallelToolCalls any `json:"parallel_tool_calls,omitempty"` + // Store can be set to true to store the output of this completion request for use in distillations and evals. + // https://platform.openai.com/docs/api-reference/chat/create#chat-create-store + Store bool `json:"store,omitempty"` + // Metadata to store with the completion. + Metadata map[string]string `json:"metadata,omitempty"` +} + +type StreamOptions struct { + // If set, an additional chunk will be streamed before the data: [DONE] message. + // The usage field on this chunk shows the token usage statistics for the entire request, + // and the choices field will always be an empty array. + // All other chunks will also include a usage field, but with a null value. + IncludeUsage bool `json:"include_usage,omitempty"` +} + +type ToolType string + +const ( + ToolTypeFunction ToolType = "function" +) + +type Tool struct { + Type ToolType `json:"type"` + Function *FunctionDefinition `json:"function,omitempty"` +} + +type ToolChoice struct { + Type ToolType `json:"type"` + Function ToolFunction `json:"function,omitempty"` +} + +type ToolFunction struct { + Name string `json:"name"` +} + +type FunctionDefinition struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Strict bool `json:"strict,omitempty"` + // Parameters is an object describing the function. + // You can pass json.RawMessage to describe the schema, + // or you can pass in a struct which serializes to the proper JSON schema. + // The jsonschema package is provided for convenience, but you should + // consider another specialized library if you require more complex schemas. + Parameters any `json:"parameters"` +} + +// Deprecated: use FunctionDefinition instead. +type FunctionDefine = FunctionDefinition + +type TopLogProbs struct { + Token string `json:"token"` + LogProb float64 `json:"logprob"` + Bytes []byte `json:"bytes,omitempty"` +} + +// LogProb represents the probability information for a token. +type LogProb struct { + Token string `json:"token"` + LogProb float64 `json:"logprob"` + Bytes []byte `json:"bytes,omitempty"` // Omitting the field if it is null + // TopLogProbs is a list of the most likely tokens and their log probability, at this token position. + // In rare cases, there may be fewer than the number of requested top_logprobs returned. + TopLogProbs []TopLogProbs `json:"top_logprobs"` +} + +// LogProbs is the top-level structure containing the log probability information. +type LogProbs struct { + // Content is a list of message content tokens with log probability information. + Content []LogProb `json:"content"` +} + +type FinishReason string + +const ( + FinishReasonStop FinishReason = "stop" + FinishReasonLength FinishReason = "length" + FinishReasonFunctionCall FinishReason = "function_call" + FinishReasonToolCalls FinishReason = "tool_calls" + FinishReasonContentFilter FinishReason = "content_filter" + FinishReasonNull FinishReason = "null" +) + +func (r FinishReason) MarshalJSON() ([]byte, error) { + if r == FinishReasonNull || r == "" { + return []byte("null"), nil + } + return []byte(`"` + string(r) + `"`), nil // best effort to not break future API changes +} + +type ChatCompletionChoice struct { + Index int `json:"index"` + Message ChatCompletionMessage `json:"message"` + // FinishReason + // stop: API returned complete message, + // or a message terminated by one of the stop sequences provided via the stop parameter + // length: Incomplete model output due to max_tokens parameter or token limit + // function_call: The model decided to call a function + // content_filter: Omitted content due to a flag from our content filters + // null: API response still in progress or incomplete + FinishReason FinishReason `json:"finish_reason"` + LogProbs *LogProbs `json:"logprobs,omitempty"` + ContentFilterResults ContentFilterResults `json:"content_filter_results,omitempty"` +} + +// ChatCompletionResponse represents a response structure for chat completion API. +type ChatCompletionResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []ChatCompletionChoice `json:"choices"` + Usage Usage `json:"usage"` + SystemFingerprint string `json:"system_fingerprint"` + PromptFilterResults []PromptFilterResult `json:"prompt_filter_results,omitempty"` + + httpHeader +} + +// CreateChatCompletion — API call to Create a completion for the chat message. +func (c *Client) CreateChatCompletion( + ctx context.Context, + request ChatCompletionRequest, +) (response ChatCompletionResponse, err error) { + if request.Stream { + err = ErrChatCompletionStreamNotSupported + return + } + + urlSuffix := chatCompletionsSuffix + if !checkEndpointSupportsModel(urlSuffix, request.Model) { + err = ErrChatCompletionInvalidModel + return + } + + if err = validateRequestForO1Models(request); err != nil { + return + } + + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix, withModel(request.Model)), + withBody(request), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/chat_stream.go b/vendor/github.com/sashabaranov/go-openai/chat_stream.go new file mode 100644 index 00000000..58b2651c --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/chat_stream.go @@ -0,0 +1,105 @@ +package openai + +import ( + "context" + "net/http" +) + +type ChatCompletionStreamChoiceDelta struct { + Content string `json:"content,omitempty"` + Role string `json:"role,omitempty"` + FunctionCall *FunctionCall `json:"function_call,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + Refusal string `json:"refusal,omitempty"` +} + +type ChatCompletionStreamChoiceLogprobs struct { + Content []ChatCompletionTokenLogprob `json:"content,omitempty"` + Refusal []ChatCompletionTokenLogprob `json:"refusal,omitempty"` +} + +type ChatCompletionTokenLogprob struct { + Token string `json:"token"` + Bytes []int64 `json:"bytes,omitempty"` + Logprob float64 `json:"logprob,omitempty"` + TopLogprobs []ChatCompletionTokenLogprobTopLogprob `json:"top_logprobs"` +} + +type ChatCompletionTokenLogprobTopLogprob struct { + Token string `json:"token"` + Bytes []int64 `json:"bytes"` + Logprob float64 `json:"logprob"` +} + +type ChatCompletionStreamChoice struct { + Index int `json:"index"` + Delta ChatCompletionStreamChoiceDelta `json:"delta"` + Logprobs *ChatCompletionStreamChoiceLogprobs `json:"logprobs,omitempty"` + FinishReason FinishReason `json:"finish_reason"` + ContentFilterResults ContentFilterResults `json:"content_filter_results,omitempty"` +} + +type PromptFilterResult struct { + Index int `json:"index"` + ContentFilterResults ContentFilterResults `json:"content_filter_results,omitempty"` +} + +type ChatCompletionStreamResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []ChatCompletionStreamChoice `json:"choices"` + SystemFingerprint string `json:"system_fingerprint"` + PromptAnnotations []PromptAnnotation `json:"prompt_annotations,omitempty"` + PromptFilterResults []PromptFilterResult `json:"prompt_filter_results,omitempty"` + // An optional field that will only be present when you set stream_options: {"include_usage": true} in your request. + // When present, it contains a null value except for the last chunk which contains the token usage statistics + // for the entire request. + Usage *Usage `json:"usage,omitempty"` +} + +// ChatCompletionStream +// Note: Perhaps it is more elegant to abstract Stream using generics. +type ChatCompletionStream struct { + *streamReader[ChatCompletionStreamResponse] +} + +// CreateChatCompletionStream — API call to create a chat completion w/ streaming +// support. It sets whether to stream back partial progress. If set, tokens will be +// sent as data-only server-sent events as they become available, with the +// stream terminated by a data: [DONE] message. +func (c *Client) CreateChatCompletionStream( + ctx context.Context, + request ChatCompletionRequest, +) (stream *ChatCompletionStream, err error) { + urlSuffix := chatCompletionsSuffix + if !checkEndpointSupportsModel(urlSuffix, request.Model) { + err = ErrChatCompletionInvalidModel + return + } + + request.Stream = true + if err = validateRequestForO1Models(request); err != nil { + return + } + + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix, withModel(request.Model)), + withBody(request), + ) + if err != nil { + return nil, err + } + + resp, err := sendRequestStream[ChatCompletionStreamResponse](c, req) + if err != nil { + return + } + stream = &ChatCompletionStream{ + streamReader: resp, + } + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/client.go b/vendor/github.com/sashabaranov/go-openai/client.go new file mode 100644 index 00000000..ed8595e0 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/client.go @@ -0,0 +1,319 @@ +package openai + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + utils "github.com/sashabaranov/go-openai/internal" +) + +// Client is OpenAI GPT-3 API client. +type Client struct { + config ClientConfig + + requestBuilder utils.RequestBuilder + createFormBuilder func(io.Writer) utils.FormBuilder +} + +type Response interface { + SetHeader(http.Header) +} + +type httpHeader http.Header + +func (h *httpHeader) SetHeader(header http.Header) { + *h = httpHeader(header) +} + +func (h *httpHeader) Header() http.Header { + return http.Header(*h) +} + +func (h *httpHeader) GetRateLimitHeaders() RateLimitHeaders { + return newRateLimitHeaders(h.Header()) +} + +type RawResponse struct { + io.ReadCloser + + httpHeader +} + +// NewClient creates new OpenAI API client. +func NewClient(authToken string) *Client { + config := DefaultConfig(authToken) + return NewClientWithConfig(config) +} + +// NewClientWithConfig creates new OpenAI API client for specified config. +func NewClientWithConfig(config ClientConfig) *Client { + return &Client{ + config: config, + requestBuilder: utils.NewRequestBuilder(), + createFormBuilder: func(body io.Writer) utils.FormBuilder { + return utils.NewFormBuilder(body) + }, + } +} + +// NewOrgClient creates new OpenAI API client for specified Organization ID. +// +// Deprecated: Please use NewClientWithConfig. +func NewOrgClient(authToken, org string) *Client { + config := DefaultConfig(authToken) + config.OrgID = org + return NewClientWithConfig(config) +} + +type requestOptions struct { + body any + header http.Header +} + +type requestOption func(*requestOptions) + +func withBody(body any) requestOption { + return func(args *requestOptions) { + args.body = body + } +} + +func withContentType(contentType string) requestOption { + return func(args *requestOptions) { + args.header.Set("Content-Type", contentType) + } +} + +func withBetaAssistantVersion(version string) requestOption { + return func(args *requestOptions) { + args.header.Set("OpenAI-Beta", fmt.Sprintf("assistants=%s", version)) + } +} + +func (c *Client) newRequest(ctx context.Context, method, url string, setters ...requestOption) (*http.Request, error) { + // Default Options + args := &requestOptions{ + body: nil, + header: make(http.Header), + } + for _, setter := range setters { + setter(args) + } + req, err := c.requestBuilder.Build(ctx, method, url, args.body, args.header) + if err != nil { + return nil, err + } + c.setCommonHeaders(req) + return req, nil +} + +func (c *Client) sendRequest(req *http.Request, v Response) error { + req.Header.Set("Accept", "application/json") + + // Check whether Content-Type is already set, Upload Files API requires + // Content-Type == multipart/form-data + contentType := req.Header.Get("Content-Type") + if contentType == "" { + req.Header.Set("Content-Type", "application/json") + } + + res, err := c.config.HTTPClient.Do(req) + if err != nil { + return err + } + + defer res.Body.Close() + + if v != nil { + v.SetHeader(res.Header) + } + + if isFailureStatusCode(res) { + return c.handleErrorResp(res) + } + + return decodeResponse(res.Body, v) +} + +func (c *Client) sendRequestRaw(req *http.Request) (response RawResponse, err error) { + resp, err := c.config.HTTPClient.Do(req) //nolint:bodyclose // body should be closed by outer function + if err != nil { + return + } + + if isFailureStatusCode(resp) { + err = c.handleErrorResp(resp) + return + } + + response.SetHeader(resp.Header) + response.ReadCloser = resp.Body + return +} + +func sendRequestStream[T streamable](client *Client, req *http.Request) (*streamReader[T], error) { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Connection", "keep-alive") + + resp, err := client.config.HTTPClient.Do(req) //nolint:bodyclose // body is closed in stream.Close() + if err != nil { + return new(streamReader[T]), err + } + if isFailureStatusCode(resp) { + return new(streamReader[T]), client.handleErrorResp(resp) + } + return &streamReader[T]{ + emptyMessagesLimit: client.config.EmptyMessagesLimit, + reader: bufio.NewReader(resp.Body), + response: resp, + errAccumulator: utils.NewErrorAccumulator(), + unmarshaler: &utils.JSONUnmarshaler{}, + httpHeader: httpHeader(resp.Header), + }, nil +} + +func (c *Client) setCommonHeaders(req *http.Request) { + // https://learn.microsoft.com/en-us/azure/cognitive-services/openai/reference#authentication + // Azure API Key authentication + if c.config.APIType == APITypeAzure || c.config.APIType == APITypeCloudflareAzure { + req.Header.Set(AzureAPIKeyHeader, c.config.authToken) + } else if c.config.authToken != "" { + // OpenAI or Azure AD authentication + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.config.authToken)) + } + if c.config.OrgID != "" { + req.Header.Set("OpenAI-Organization", c.config.OrgID) + } +} + +func isFailureStatusCode(resp *http.Response) bool { + return resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest +} + +func decodeResponse(body io.Reader, v any) error { + if v == nil { + return nil + } + + switch o := v.(type) { + case *string: + return decodeString(body, o) + case *audioTextResponse: + return decodeString(body, &o.Text) + default: + return json.NewDecoder(body).Decode(v) + } +} + +func decodeString(body io.Reader, output *string) error { + b, err := io.ReadAll(body) + if err != nil { + return err + } + *output = string(b) + return nil +} + +type fullURLOptions struct { + model string +} + +type fullURLOption func(*fullURLOptions) + +func withModel(model string) fullURLOption { + return func(args *fullURLOptions) { + args.model = model + } +} + +var azureDeploymentsEndpoints = []string{ + "/completions", + "/embeddings", + "/chat/completions", + "/audio/transcriptions", + "/audio/translations", + "/audio/speech", + "/images/generations", +} + +// fullURL returns full URL for request. +func (c *Client) fullURL(suffix string, setters ...fullURLOption) string { + baseURL := strings.TrimRight(c.config.BaseURL, "/") + args := fullURLOptions{} + for _, setter := range setters { + setter(&args) + } + + if c.config.APIType == APITypeAzure || c.config.APIType == APITypeAzureAD { + baseURL = c.baseURLWithAzureDeployment(baseURL, suffix, args.model) + } + + if c.config.APIVersion != "" { + suffix = c.suffixWithAPIVersion(suffix) + } + return fmt.Sprintf("%s%s", baseURL, suffix) +} + +func (c *Client) suffixWithAPIVersion(suffix string) string { + parsedSuffix, err := url.Parse(suffix) + if err != nil { + panic("failed to parse url suffix") + } + query := parsedSuffix.Query() + query.Add("api-version", c.config.APIVersion) + return fmt.Sprintf("%s?%s", parsedSuffix.Path, query.Encode()) +} + +func (c *Client) baseURLWithAzureDeployment(baseURL, suffix, model string) (newBaseURL string) { + baseURL = fmt.Sprintf("%s/%s", strings.TrimRight(baseURL, "/"), azureAPIPrefix) + if containsSubstr(azureDeploymentsEndpoints, suffix) { + azureDeploymentName := c.config.GetAzureDeploymentByModel(model) + if azureDeploymentName == "" { + azureDeploymentName = "UNKNOWN" + } + baseURL = fmt.Sprintf("%s/%s/%s", baseURL, azureDeploymentsPrefix, azureDeploymentName) + } + return baseURL +} + +func (c *Client) handleErrorResp(resp *http.Response) error { + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error, reading response body: %w", err) + } + var errRes ErrorResponse + err = json.Unmarshal(body, &errRes) + if err != nil || errRes.Error == nil { + reqErr := &RequestError{ + HTTPStatus: resp.Status, + HTTPStatusCode: resp.StatusCode, + Err: err, + Body: body, + } + if errRes.Error != nil { + reqErr.Err = errRes.Error + } + return reqErr + } + + errRes.Error.HTTPStatus = resp.Status + errRes.Error.HTTPStatusCode = resp.StatusCode + return errRes.Error +} + +func containsSubstr(s []string, e string) bool { + for _, v := range s { + if strings.Contains(e, v) { + return true + } + } + return false +} diff --git a/vendor/github.com/sashabaranov/go-openai/common.go b/vendor/github.com/sashabaranov/go-openai/common.go new file mode 100644 index 00000000..8cc7289c --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/common.go @@ -0,0 +1,24 @@ +package openai + +// common.go defines common types used throughout the OpenAI API. + +// Usage Represents the total token usage per request to OpenAI. +type Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + PromptTokensDetails *PromptTokensDetails `json:"prompt_tokens_details"` + CompletionTokensDetails *CompletionTokensDetails `json:"completion_tokens_details"` +} + +// CompletionTokensDetails Breakdown of tokens used in a completion. +type CompletionTokensDetails struct { + AudioTokens int `json:"audio_tokens"` + ReasoningTokens int `json:"reasoning_tokens"` +} + +// PromptTokensDetails Breakdown of tokens used in the prompt. +type PromptTokensDetails struct { + AudioTokens int `json:"audio_tokens"` + CachedTokens int `json:"cached_tokens"` +} diff --git a/vendor/github.com/sashabaranov/go-openai/completion.go b/vendor/github.com/sashabaranov/go-openai/completion.go new file mode 100644 index 00000000..77ea8c3a --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/completion.go @@ -0,0 +1,341 @@ +package openai + +import ( + "context" + "errors" + "net/http" +) + +var ( + ErrO1MaxTokensDeprecated = errors.New("this model is not supported MaxTokens, please use MaxCompletionTokens") //nolint:lll + ErrCompletionUnsupportedModel = errors.New("this model is not supported with this method, please use CreateChatCompletion client method instead") //nolint:lll + ErrCompletionStreamNotSupported = errors.New("streaming is not supported with this method, please use CreateCompletionStream") //nolint:lll + ErrCompletionRequestPromptTypeNotSupported = errors.New("the type of CompletionRequest.Prompt only supports string and []string") //nolint:lll +) + +var ( + ErrO1BetaLimitationsMessageTypes = errors.New("this model has beta-limitations, user and assistant messages only, system messages are not supported") //nolint:lll + ErrO1BetaLimitationsStreaming = errors.New("this model has beta-limitations, streaming not supported") //nolint:lll + ErrO1BetaLimitationsTools = errors.New("this model has beta-limitations, tools, function calling, and response format parameters are not supported") //nolint:lll + ErrO1BetaLimitationsLogprobs = errors.New("this model has beta-limitations, logprobs not supported") //nolint:lll + ErrO1BetaLimitationsOther = errors.New("this model has beta-limitations, temperature, top_p and n are fixed at 1, while presence_penalty and frequency_penalty are fixed at 0") //nolint:lll +) + +// GPT3 Defines the models provided by OpenAI to use when generating +// completions from OpenAI. +// GPT3 Models are designed for text-based tasks. For code-specific +// tasks, please refer to the Codex series of models. +const ( + O1Mini = "o1-mini" + O1Mini20240912 = "o1-mini-2024-09-12" + O1Preview = "o1-preview" + O1Preview20240912 = "o1-preview-2024-09-12" + GPT432K0613 = "gpt-4-32k-0613" + GPT432K0314 = "gpt-4-32k-0314" + GPT432K = "gpt-4-32k" + GPT40613 = "gpt-4-0613" + GPT40314 = "gpt-4-0314" + GPT4o = "gpt-4o" + GPT4o20240513 = "gpt-4o-2024-05-13" + GPT4o20240806 = "gpt-4o-2024-08-06" + GPT4oLatest = "chatgpt-4o-latest" + GPT4oMini = "gpt-4o-mini" + GPT4oMini20240718 = "gpt-4o-mini-2024-07-18" + GPT4Turbo = "gpt-4-turbo" + GPT4Turbo20240409 = "gpt-4-turbo-2024-04-09" + GPT4Turbo0125 = "gpt-4-0125-preview" + GPT4Turbo1106 = "gpt-4-1106-preview" + GPT4TurboPreview = "gpt-4-turbo-preview" + GPT4VisionPreview = "gpt-4-vision-preview" + GPT4 = "gpt-4" + GPT3Dot5Turbo0125 = "gpt-3.5-turbo-0125" + GPT3Dot5Turbo1106 = "gpt-3.5-turbo-1106" + GPT3Dot5Turbo0613 = "gpt-3.5-turbo-0613" + GPT3Dot5Turbo0301 = "gpt-3.5-turbo-0301" + GPT3Dot5Turbo16K = "gpt-3.5-turbo-16k" + GPT3Dot5Turbo16K0613 = "gpt-3.5-turbo-16k-0613" + GPT3Dot5Turbo = "gpt-3.5-turbo" + GPT3Dot5TurboInstruct = "gpt-3.5-turbo-instruct" + // Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead. + GPT3TextDavinci003 = "text-davinci-003" + // Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead. + GPT3TextDavinci002 = "text-davinci-002" + // Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead. + GPT3TextCurie001 = "text-curie-001" + // Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead. + GPT3TextBabbage001 = "text-babbage-001" + // Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead. + GPT3TextAda001 = "text-ada-001" + // Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead. + GPT3TextDavinci001 = "text-davinci-001" + // Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead. + GPT3DavinciInstructBeta = "davinci-instruct-beta" + // Deprecated: Model is shutdown. Use davinci-002 instead. + GPT3Davinci = "davinci" + GPT3Davinci002 = "davinci-002" + // Deprecated: Model is shutdown. Use gpt-3.5-turbo-instruct instead. + GPT3CurieInstructBeta = "curie-instruct-beta" + GPT3Curie = "curie" + GPT3Curie002 = "curie-002" + // Deprecated: Model is shutdown. Use babbage-002 instead. + GPT3Ada = "ada" + GPT3Ada002 = "ada-002" + // Deprecated: Model is shutdown. Use babbage-002 instead. + GPT3Babbage = "babbage" + GPT3Babbage002 = "babbage-002" +) + +// Codex Defines the models provided by OpenAI. +// These models are designed for code-specific tasks, and use +// a different tokenizer which optimizes for whitespace. +const ( + CodexCodeDavinci002 = "code-davinci-002" + CodexCodeCushman001 = "code-cushman-001" + CodexCodeDavinci001 = "code-davinci-001" +) + +// O1SeriesModels List of new Series of OpenAI models. +// Some old api attributes not supported. +var O1SeriesModels = map[string]struct{}{ + O1Mini: {}, + O1Mini20240912: {}, + O1Preview: {}, + O1Preview20240912: {}, +} + +var disabledModelsForEndpoints = map[string]map[string]bool{ + "/completions": { + O1Mini: true, + O1Mini20240912: true, + O1Preview: true, + O1Preview20240912: true, + GPT3Dot5Turbo: true, + GPT3Dot5Turbo0301: true, + GPT3Dot5Turbo0613: true, + GPT3Dot5Turbo1106: true, + GPT3Dot5Turbo0125: true, + GPT3Dot5Turbo16K: true, + GPT3Dot5Turbo16K0613: true, + GPT4: true, + GPT4o: true, + GPT4o20240513: true, + GPT4o20240806: true, + GPT4oLatest: true, + GPT4oMini: true, + GPT4oMini20240718: true, + GPT4TurboPreview: true, + GPT4VisionPreview: true, + GPT4Turbo1106: true, + GPT4Turbo0125: true, + GPT4Turbo: true, + GPT4Turbo20240409: true, + GPT40314: true, + GPT40613: true, + GPT432K: true, + GPT432K0314: true, + GPT432K0613: true, + }, + chatCompletionsSuffix: { + CodexCodeDavinci002: true, + CodexCodeCushman001: true, + CodexCodeDavinci001: true, + GPT3TextDavinci003: true, + GPT3TextDavinci002: true, + GPT3TextCurie001: true, + GPT3TextBabbage001: true, + GPT3TextAda001: true, + GPT3TextDavinci001: true, + GPT3DavinciInstructBeta: true, + GPT3Davinci: true, + GPT3CurieInstructBeta: true, + GPT3Curie: true, + GPT3Ada: true, + GPT3Babbage: true, + }, +} + +func checkEndpointSupportsModel(endpoint, model string) bool { + return !disabledModelsForEndpoints[endpoint][model] +} + +func checkPromptType(prompt any) bool { + _, isString := prompt.(string) + _, isStringSlice := prompt.([]string) + if isString || isStringSlice { + return true + } + + // check if it is prompt is []string hidden under []any + slice, isSlice := prompt.([]any) + if !isSlice { + return false + } + + for _, item := range slice { + _, itemIsString := item.(string) + if !itemIsString { + return false + } + } + return true // all items in the slice are string, so it is []string +} + +var unsupportedToolsForO1Models = map[ToolType]struct{}{ + ToolTypeFunction: {}, +} + +var availableMessageRoleForO1Models = map[string]struct{}{ + ChatMessageRoleUser: {}, + ChatMessageRoleAssistant: {}, +} + +// validateRequestForO1Models checks for deprecated fields of OpenAI models. +func validateRequestForO1Models(request ChatCompletionRequest) error { + if _, found := O1SeriesModels[request.Model]; !found { + return nil + } + + if request.MaxTokens > 0 { + return ErrO1MaxTokensDeprecated + } + + // Beta Limitations + // refs:https://platform.openai.com/docs/guides/reasoning/beta-limitations + // Streaming: not supported + if request.Stream { + return ErrO1BetaLimitationsStreaming + } + // Logprobs: not supported. + if request.LogProbs { + return ErrO1BetaLimitationsLogprobs + } + + // Message types: user and assistant messages only, system messages are not supported. + for _, m := range request.Messages { + if _, found := availableMessageRoleForO1Models[m.Role]; !found { + return ErrO1BetaLimitationsMessageTypes + } + } + + // Tools: tools, function calling, and response format parameters are not supported + for _, t := range request.Tools { + if _, found := unsupportedToolsForO1Models[t.Type]; found { + return ErrO1BetaLimitationsTools + } + } + + // Other: temperature, top_p and n are fixed at 1, while presence_penalty and frequency_penalty are fixed at 0. + if request.Temperature > 0 && request.Temperature != 1 { + return ErrO1BetaLimitationsOther + } + if request.TopP > 0 && request.TopP != 1 { + return ErrO1BetaLimitationsOther + } + if request.N > 0 && request.N != 1 { + return ErrO1BetaLimitationsOther + } + if request.PresencePenalty > 0 { + return ErrO1BetaLimitationsOther + } + if request.FrequencyPenalty > 0 { + return ErrO1BetaLimitationsOther + } + + return nil +} + +// CompletionRequest represents a request structure for completion API. +type CompletionRequest struct { + Model string `json:"model"` + Prompt any `json:"prompt,omitempty"` + BestOf int `json:"best_of,omitempty"` + Echo bool `json:"echo,omitempty"` + FrequencyPenalty float32 `json:"frequency_penalty,omitempty"` + // LogitBias is must be a token id string (specified by their token ID in the tokenizer), not a word string. + // incorrect: `"logit_bias":{"You": 6}`, correct: `"logit_bias":{"1639": 6}` + // refs: https://platform.openai.com/docs/api-reference/completions/create#completions/create-logit_bias + LogitBias map[string]int `json:"logit_bias,omitempty"` + // Store can be set to true to store the output of this completion request for use in distillations and evals. + // https://platform.openai.com/docs/api-reference/chat/create#chat-create-store + Store bool `json:"store,omitempty"` + // Metadata to store with the completion. + Metadata map[string]string `json:"metadata,omitempty"` + LogProbs int `json:"logprobs,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` + N int `json:"n,omitempty"` + PresencePenalty float32 `json:"presence_penalty,omitempty"` + Seed *int `json:"seed,omitempty"` + Stop []string `json:"stop,omitempty"` + Stream bool `json:"stream,omitempty"` + Suffix string `json:"suffix,omitempty"` + Temperature float32 `json:"temperature,omitempty"` + TopP float32 `json:"top_p,omitempty"` + User string `json:"user,omitempty"` +} + +// CompletionChoice represents one of possible completions. +type CompletionChoice struct { + Text string `json:"text"` + Index int `json:"index"` + FinishReason string `json:"finish_reason"` + LogProbs LogprobResult `json:"logprobs"` +} + +// LogprobResult represents logprob result of Choice. +type LogprobResult struct { + Tokens []string `json:"tokens"` + TokenLogprobs []float32 `json:"token_logprobs"` + TopLogprobs []map[string]float32 `json:"top_logprobs"` + TextOffset []int `json:"text_offset"` +} + +// CompletionResponse represents a response structure for completion API. +type CompletionResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []CompletionChoice `json:"choices"` + Usage Usage `json:"usage"` + + httpHeader +} + +// CreateCompletion — API call to create a completion. This is the main endpoint of the API. Returns new text as well +// as, if requested, the probabilities over each alternative token at each position. +// +// If using a fine-tuned model, simply provide the model's ID in the CompletionRequest object, +// and the server will use the model's parameters to generate the completion. +func (c *Client) CreateCompletion( + ctx context.Context, + request CompletionRequest, +) (response CompletionResponse, err error) { + if request.Stream { + err = ErrCompletionStreamNotSupported + return + } + + urlSuffix := "/completions" + if !checkEndpointSupportsModel(urlSuffix, request.Model) { + err = ErrCompletionUnsupportedModel + return + } + + if !checkPromptType(request.Prompt) { + err = ErrCompletionRequestPromptTypeNotSupported + return + } + + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix, withModel(request.Model)), + withBody(request), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/config.go b/vendor/github.com/sashabaranov/go-openai/config.go new file mode 100644 index 00000000..8a918355 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/config.go @@ -0,0 +1,89 @@ +package openai + +import ( + "net/http" + "regexp" +) + +const ( + openaiAPIURLv1 = "https://api.openai.com/v1" + defaultEmptyMessagesLimit uint = 300 + + azureAPIPrefix = "openai" + azureDeploymentsPrefix = "deployments" +) + +type APIType string + +const ( + APITypeOpenAI APIType = "OPEN_AI" + APITypeAzure APIType = "AZURE" + APITypeAzureAD APIType = "AZURE_AD" + APITypeCloudflareAzure APIType = "CLOUDFLARE_AZURE" +) + +const AzureAPIKeyHeader = "api-key" + +const defaultAssistantVersion = "v2" // upgrade to v2 to support vector store + +type HTTPDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// ClientConfig is a configuration of a client. +type ClientConfig struct { + authToken string + + BaseURL string + OrgID string + APIType APIType + APIVersion string // required when APIType is APITypeAzure or APITypeAzureAD + AssistantVersion string + AzureModelMapperFunc func(model string) string // replace model to azure deployment name func + HTTPClient HTTPDoer + + EmptyMessagesLimit uint +} + +func DefaultConfig(authToken string) ClientConfig { + return ClientConfig{ + authToken: authToken, + BaseURL: openaiAPIURLv1, + APIType: APITypeOpenAI, + AssistantVersion: defaultAssistantVersion, + OrgID: "", + + HTTPClient: &http.Client{}, + + EmptyMessagesLimit: defaultEmptyMessagesLimit, + } +} + +func DefaultAzureConfig(apiKey, baseURL string) ClientConfig { + return ClientConfig{ + authToken: apiKey, + BaseURL: baseURL, + OrgID: "", + APIType: APITypeAzure, + APIVersion: "2023-05-15", + AzureModelMapperFunc: func(model string) string { + return regexp.MustCompile(`[.:]`).ReplaceAllString(model, "") + }, + + HTTPClient: &http.Client{}, + + EmptyMessagesLimit: defaultEmptyMessagesLimit, + } +} + +func (ClientConfig) String() string { + return "" +} + +func (c ClientConfig) GetAzureDeploymentByModel(model string) string { + if c.AzureModelMapperFunc != nil { + return c.AzureModelMapperFunc(model) + } + + return model +} diff --git a/vendor/github.com/sashabaranov/go-openai/edits.go b/vendor/github.com/sashabaranov/go-openai/edits.go new file mode 100644 index 00000000..fe8ecd0c --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/edits.go @@ -0,0 +1,53 @@ +package openai + +import ( + "context" + "fmt" + "net/http" +) + +// EditsRequest represents a request structure for Edits API. +type EditsRequest struct { + Model *string `json:"model,omitempty"` + Input string `json:"input,omitempty"` + Instruction string `json:"instruction,omitempty"` + N int `json:"n,omitempty"` + Temperature float32 `json:"temperature,omitempty"` + TopP float32 `json:"top_p,omitempty"` +} + +// EditsChoice represents one of possible edits. +type EditsChoice struct { + Text string `json:"text"` + Index int `json:"index"` +} + +// EditsResponse represents a response structure for Edits API. +type EditsResponse struct { + Object string `json:"object"` + Created int64 `json:"created"` + Usage Usage `json:"usage"` + Choices []EditsChoice `json:"choices"` + + httpHeader +} + +// Edits Perform an API call to the Edits endpoint. +/* Deprecated: Users of the Edits API and its associated models (e.g., text-davinci-edit-001 or code-davinci-edit-001) +will need to migrate to GPT-3.5 Turbo by January 4, 2024. +You can use CreateChatCompletion or CreateChatCompletionStream instead. +*/ +func (c *Client) Edits(ctx context.Context, request EditsRequest) (response EditsResponse, err error) { + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL("/edits", withModel(fmt.Sprint(request.Model))), + withBody(request), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/embeddings.go b/vendor/github.com/sashabaranov/go-openai/embeddings.go new file mode 100644 index 00000000..74eb8aa5 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/embeddings.go @@ -0,0 +1,267 @@ +package openai + +import ( + "context" + "encoding/base64" + "encoding/binary" + "errors" + "math" + "net/http" +) + +var ErrVectorLengthMismatch = errors.New("vector length mismatch") + +// EmbeddingModel enumerates the models which can be used +// to generate Embedding vectors. +type EmbeddingModel string + +const ( + // Deprecated: The following block is shut down. Use text-embedding-ada-002 instead. + AdaSimilarity EmbeddingModel = "text-similarity-ada-001" + BabbageSimilarity EmbeddingModel = "text-similarity-babbage-001" + CurieSimilarity EmbeddingModel = "text-similarity-curie-001" + DavinciSimilarity EmbeddingModel = "text-similarity-davinci-001" + AdaSearchDocument EmbeddingModel = "text-search-ada-doc-001" + AdaSearchQuery EmbeddingModel = "text-search-ada-query-001" + BabbageSearchDocument EmbeddingModel = "text-search-babbage-doc-001" + BabbageSearchQuery EmbeddingModel = "text-search-babbage-query-001" + CurieSearchDocument EmbeddingModel = "text-search-curie-doc-001" + CurieSearchQuery EmbeddingModel = "text-search-curie-query-001" + DavinciSearchDocument EmbeddingModel = "text-search-davinci-doc-001" + DavinciSearchQuery EmbeddingModel = "text-search-davinci-query-001" + AdaCodeSearchCode EmbeddingModel = "code-search-ada-code-001" + AdaCodeSearchText EmbeddingModel = "code-search-ada-text-001" + BabbageCodeSearchCode EmbeddingModel = "code-search-babbage-code-001" + BabbageCodeSearchText EmbeddingModel = "code-search-babbage-text-001" + + AdaEmbeddingV2 EmbeddingModel = "text-embedding-ada-002" + SmallEmbedding3 EmbeddingModel = "text-embedding-3-small" + LargeEmbedding3 EmbeddingModel = "text-embedding-3-large" +) + +// Embedding is a special format of data representation that can be easily utilized by machine +// learning models and algorithms. The embedding is an information dense representation of the +// semantic meaning of a piece of text. Each embedding is a vector of floating point numbers, +// such that the distance between two embeddings in the vector space is correlated with semantic similarity +// between two inputs in the original format. For example, if two texts are similar, +// then their vector representations should also be similar. +type Embedding struct { + Object string `json:"object"` + Embedding []float32 `json:"embedding"` + Index int `json:"index"` +} + +// DotProduct calculates the dot product of the embedding vector with another +// embedding vector. Both vectors must have the same length; otherwise, an +// ErrVectorLengthMismatch is returned. The method returns the calculated dot +// product as a float32 value. +func (e *Embedding) DotProduct(other *Embedding) (float32, error) { + if len(e.Embedding) != len(other.Embedding) { + return 0, ErrVectorLengthMismatch + } + + var dotProduct float32 + for i := range e.Embedding { + dotProduct += e.Embedding[i] * other.Embedding[i] + } + + return dotProduct, nil +} + +// EmbeddingResponse is the response from a Create embeddings request. +type EmbeddingResponse struct { + Object string `json:"object"` + Data []Embedding `json:"data"` + Model EmbeddingModel `json:"model"` + Usage Usage `json:"usage"` + + httpHeader +} + +type base64String string + +func (b base64String) Decode() ([]float32, error) { + decodedData, err := base64.StdEncoding.DecodeString(string(b)) + if err != nil { + return nil, err + } + + const sizeOfFloat32 = 4 + floats := make([]float32, len(decodedData)/sizeOfFloat32) + for i := 0; i < len(floats); i++ { + floats[i] = math.Float32frombits(binary.LittleEndian.Uint32(decodedData[i*4 : (i+1)*4])) + } + + return floats, nil +} + +// Base64Embedding is a container for base64 encoded embeddings. +type Base64Embedding struct { + Object string `json:"object"` + Embedding base64String `json:"embedding"` + Index int `json:"index"` +} + +// EmbeddingResponseBase64 is the response from a Create embeddings request with base64 encoding format. +type EmbeddingResponseBase64 struct { + Object string `json:"object"` + Data []Base64Embedding `json:"data"` + Model EmbeddingModel `json:"model"` + Usage Usage `json:"usage"` + + httpHeader +} + +// ToEmbeddingResponse converts an embeddingResponseBase64 to an EmbeddingResponse. +func (r *EmbeddingResponseBase64) ToEmbeddingResponse() (EmbeddingResponse, error) { + data := make([]Embedding, len(r.Data)) + + for i, base64Embedding := range r.Data { + embedding, err := base64Embedding.Embedding.Decode() + if err != nil { + return EmbeddingResponse{}, err + } + + data[i] = Embedding{ + Object: base64Embedding.Object, + Embedding: embedding, + Index: base64Embedding.Index, + } + } + + return EmbeddingResponse{ + Object: r.Object, + Model: r.Model, + Data: data, + Usage: r.Usage, + }, nil +} + +type EmbeddingRequestConverter interface { + // Needs to be of type EmbeddingRequestStrings or EmbeddingRequestTokens + Convert() EmbeddingRequest +} + +// EmbeddingEncodingFormat is the format of the embeddings data. +// Currently, only "float" and "base64" are supported, however, "base64" is not officially documented. +// If not specified OpenAI will use "float". +type EmbeddingEncodingFormat string + +const ( + EmbeddingEncodingFormatFloat EmbeddingEncodingFormat = "float" + EmbeddingEncodingFormatBase64 EmbeddingEncodingFormat = "base64" +) + +type EmbeddingRequest struct { + Input any `json:"input"` + Model EmbeddingModel `json:"model"` + User string `json:"user"` + EncodingFormat EmbeddingEncodingFormat `json:"encoding_format,omitempty"` + // Dimensions The number of dimensions the resulting output embeddings should have. + // Only supported in text-embedding-3 and later models. + Dimensions int `json:"dimensions,omitempty"` +} + +func (r EmbeddingRequest) Convert() EmbeddingRequest { + return r +} + +// EmbeddingRequestStrings is the input to a create embeddings request with a slice of strings. +type EmbeddingRequestStrings struct { + // Input is a slice of strings for which you want to generate an Embedding vector. + // Each input must not exceed 8192 tokens in length. + // OpenAPI suggests replacing newlines (\n) in your input with a single space, as they + // have observed inferior results when newlines are present. + // E.g. + // "The food was delicious and the waiter..." + Input []string `json:"input"` + // ID of the model to use. You can use the List models API to see all of your available models, + // or see our Model overview for descriptions of them. + Model EmbeddingModel `json:"model"` + // A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. + User string `json:"user"` + // EmbeddingEncodingFormat is the format of the embeddings data. + // Currently, only "float" and "base64" are supported, however, "base64" is not officially documented. + // If not specified OpenAI will use "float". + EncodingFormat EmbeddingEncodingFormat `json:"encoding_format,omitempty"` + // Dimensions The number of dimensions the resulting output embeddings should have. + // Only supported in text-embedding-3 and later models. + Dimensions int `json:"dimensions,omitempty"` +} + +func (r EmbeddingRequestStrings) Convert() EmbeddingRequest { + return EmbeddingRequest{ + Input: r.Input, + Model: r.Model, + User: r.User, + EncodingFormat: r.EncodingFormat, + Dimensions: r.Dimensions, + } +} + +type EmbeddingRequestTokens struct { + // Input is a slice of slices of ints ([][]int) for which you want to generate an Embedding vector. + // Each input must not exceed 8192 tokens in length. + // OpenAPI suggests replacing newlines (\n) in your input with a single space, as they + // have observed inferior results when newlines are present. + // E.g. + // "The food was delicious and the waiter..." + Input [][]int `json:"input"` + // ID of the model to use. You can use the List models API to see all of your available models, + // or see our Model overview for descriptions of them. + Model EmbeddingModel `json:"model"` + // A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. + User string `json:"user"` + // EmbeddingEncodingFormat is the format of the embeddings data. + // Currently, only "float" and "base64" are supported, however, "base64" is not officially documented. + // If not specified OpenAI will use "float". + EncodingFormat EmbeddingEncodingFormat `json:"encoding_format,omitempty"` + // Dimensions The number of dimensions the resulting output embeddings should have. + // Only supported in text-embedding-3 and later models. + Dimensions int `json:"dimensions,omitempty"` +} + +func (r EmbeddingRequestTokens) Convert() EmbeddingRequest { + return EmbeddingRequest{ + Input: r.Input, + Model: r.Model, + User: r.User, + EncodingFormat: r.EncodingFormat, + Dimensions: r.Dimensions, + } +} + +// CreateEmbeddings returns an EmbeddingResponse which will contain an Embedding for every item in |body.Input|. +// https://beta.openai.com/docs/api-reference/embeddings/create +// +// Body should be of type EmbeddingRequestStrings for embedding strings or EmbeddingRequestTokens +// for embedding groups of text already converted to tokens. +func (c *Client) CreateEmbeddings( + ctx context.Context, + conv EmbeddingRequestConverter, +) (res EmbeddingResponse, err error) { + baseReq := conv.Convert() + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL("/embeddings", withModel(string(baseReq.Model))), + withBody(baseReq), + ) + if err != nil { + return + } + + if baseReq.EncodingFormat != EmbeddingEncodingFormatBase64 { + err = c.sendRequest(req, &res) + return + } + + base64Response := &EmbeddingResponseBase64{} + err = c.sendRequest(req, base64Response) + if err != nil { + return + } + + res, err = base64Response.ToEmbeddingResponse() + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/engines.go b/vendor/github.com/sashabaranov/go-openai/engines.go new file mode 100644 index 00000000..5a0dba85 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/engines.go @@ -0,0 +1,52 @@ +package openai + +import ( + "context" + "fmt" + "net/http" +) + +// Engine struct represents engine from OpenAPI API. +type Engine struct { + ID string `json:"id"` + Object string `json:"object"` + Owner string `json:"owner"` + Ready bool `json:"ready"` + + httpHeader +} + +// EnginesList is a list of engines. +type EnginesList struct { + Engines []Engine `json:"data"` + + httpHeader +} + +// ListEngines Lists the currently available engines, and provides basic +// information about each option such as the owner and availability. +func (c *Client) ListEngines(ctx context.Context) (engines EnginesList, err error) { + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/engines")) + if err != nil { + return + } + + err = c.sendRequest(req, &engines) + return +} + +// GetEngine Retrieves an engine instance, providing basic information about +// the engine such as the owner and availability. +func (c *Client) GetEngine( + ctx context.Context, + engineID string, +) (engine Engine, err error) { + urlSuffix := fmt.Sprintf("/engines/%s", engineID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &engine) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/error.go b/vendor/github.com/sashabaranov/go-openai/error.go new file mode 100644 index 00000000..8a74bd52 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/error.go @@ -0,0 +1,115 @@ +package openai + +import ( + "encoding/json" + "fmt" + "strings" +) + +// APIError provides error information returned by the OpenAI API. +// InnerError struct is only valid for Azure OpenAI Service. +type APIError struct { + Code any `json:"code,omitempty"` + Message string `json:"message"` + Param *string `json:"param,omitempty"` + Type string `json:"type"` + HTTPStatus string `json:"-"` + HTTPStatusCode int `json:"-"` + InnerError *InnerError `json:"innererror,omitempty"` +} + +// InnerError Azure Content filtering. Only valid for Azure OpenAI Service. +type InnerError struct { + Code string `json:"code,omitempty"` + ContentFilterResults ContentFilterResults `json:"content_filter_result,omitempty"` +} + +// RequestError provides information about generic request errors. +type RequestError struct { + HTTPStatus string + HTTPStatusCode int + Err error + Body []byte +} + +type ErrorResponse struct { + Error *APIError `json:"error,omitempty"` +} + +func (e *APIError) Error() string { + if e.HTTPStatusCode > 0 { + return fmt.Sprintf("error, status code: %d, status: %s, message: %s", e.HTTPStatusCode, e.HTTPStatus, e.Message) + } + + return e.Message +} + +func (e *APIError) UnmarshalJSON(data []byte) (err error) { + var rawMap map[string]json.RawMessage + err = json.Unmarshal(data, &rawMap) + if err != nil { + return + } + + err = json.Unmarshal(rawMap["message"], &e.Message) + if err != nil { + // If the parameter field of a function call is invalid as a JSON schema + // refs: https://github.com/sashabaranov/go-openai/issues/381 + var messages []string + err = json.Unmarshal(rawMap["message"], &messages) + if err != nil { + return + } + e.Message = strings.Join(messages, ", ") + } + + // optional fields for azure openai + // refs: https://github.com/sashabaranov/go-openai/issues/343 + if _, ok := rawMap["type"]; ok { + err = json.Unmarshal(rawMap["type"], &e.Type) + if err != nil { + return + } + } + + if _, ok := rawMap["innererror"]; ok { + err = json.Unmarshal(rawMap["innererror"], &e.InnerError) + if err != nil { + return + } + } + + // optional fields + if _, ok := rawMap["param"]; ok { + err = json.Unmarshal(rawMap["param"], &e.Param) + if err != nil { + return + } + } + + if _, ok := rawMap["code"]; !ok { + return nil + } + + // if the api returned a number, we need to force an integer + // since the json package defaults to float64 + var intCode int + err = json.Unmarshal(rawMap["code"], &intCode) + if err == nil { + e.Code = intCode + return nil + } + + return json.Unmarshal(rawMap["code"], &e.Code) +} + +func (e *RequestError) Error() string { + return fmt.Sprintf( + "error, status code: %d, status: %s, message: %s, body: %s", + e.HTTPStatusCode, e.HTTPStatus, e.Err, e.Body, + ) +} + +func (e *RequestError) Unwrap() error { + return e.Err +} diff --git a/vendor/github.com/sashabaranov/go-openai/files.go b/vendor/github.com/sashabaranov/go-openai/files.go new file mode 100644 index 00000000..edc9f2a2 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/files.go @@ -0,0 +1,171 @@ +package openai + +import ( + "bytes" + "context" + "fmt" + "net/http" + "os" +) + +type FileRequest struct { + FileName string `json:"file"` + FilePath string `json:"-"` + Purpose string `json:"purpose"` +} + +// PurposeType represents the purpose of the file when uploading. +type PurposeType string + +const ( + PurposeFineTune PurposeType = "fine-tune" + PurposeFineTuneResults PurposeType = "fine-tune-results" + PurposeAssistants PurposeType = "assistants" + PurposeAssistantsOutput PurposeType = "assistants_output" + PurposeBatch PurposeType = "batch" +) + +// FileBytesRequest represents a file upload request. +type FileBytesRequest struct { + // the name of the uploaded file in OpenAI + Name string + // the bytes of the file + Bytes []byte + // the purpose of the file + Purpose PurposeType +} + +// File struct represents an OpenAPI file. +type File struct { + Bytes int `json:"bytes"` + CreatedAt int64 `json:"created_at"` + ID string `json:"id"` + FileName string `json:"filename"` + Object string `json:"object"` + Status string `json:"status"` + Purpose string `json:"purpose"` + StatusDetails string `json:"status_details"` + + httpHeader +} + +// FilesList is a list of files that belong to the user or organization. +type FilesList struct { + Files []File `json:"data"` + + httpHeader +} + +// CreateFileBytes uploads bytes directly to OpenAI without requiring a local file. +func (c *Client) CreateFileBytes(ctx context.Context, request FileBytesRequest) (file File, err error) { + var b bytes.Buffer + reader := bytes.NewReader(request.Bytes) + builder := c.createFormBuilder(&b) + + err = builder.WriteField("purpose", string(request.Purpose)) + if err != nil { + return + } + + err = builder.CreateFormFileReader("file", reader, request.Name) + if err != nil { + return + } + + err = builder.Close() + if err != nil { + return + } + + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/files"), + withBody(&b), withContentType(builder.FormDataContentType())) + if err != nil { + return + } + + err = c.sendRequest(req, &file) + return +} + +// CreateFile uploads a jsonl file to GPT3 +// FilePath must be a local file path. +func (c *Client) CreateFile(ctx context.Context, request FileRequest) (file File, err error) { + var b bytes.Buffer + builder := c.createFormBuilder(&b) + + err = builder.WriteField("purpose", request.Purpose) + if err != nil { + return + } + + fileData, err := os.Open(request.FilePath) + if err != nil { + return + } + defer fileData.Close() + + err = builder.CreateFormFile("file", fileData) + if err != nil { + return + } + + err = builder.Close() + if err != nil { + return + } + + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/files"), + withBody(&b), withContentType(builder.FormDataContentType())) + if err != nil { + return + } + + err = c.sendRequest(req, &file) + return +} + +// DeleteFile deletes an existing file. +func (c *Client) DeleteFile(ctx context.Context, fileID string) (err error) { + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL("/files/"+fileID)) + if err != nil { + return + } + + err = c.sendRequest(req, nil) + return +} + +// ListFiles Lists the currently available files, +// and provides basic information about each file such as the file name and purpose. +func (c *Client) ListFiles(ctx context.Context) (files FilesList, err error) { + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/files")) + if err != nil { + return + } + + err = c.sendRequest(req, &files) + return +} + +// GetFile Retrieves a file instance, providing basic information about the file +// such as the file name and purpose. +func (c *Client) GetFile(ctx context.Context, fileID string) (file File, err error) { + urlSuffix := fmt.Sprintf("/files/%s", fileID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &file) + return +} + +func (c *Client) GetFileContent(ctx context.Context, fileID string) (content RawResponse, err error) { + urlSuffix := fmt.Sprintf("/files/%s/content", fileID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + return c.sendRequestRaw(req) +} diff --git a/vendor/github.com/sashabaranov/go-openai/fine_tunes.go b/vendor/github.com/sashabaranov/go-openai/fine_tunes.go new file mode 100644 index 00000000..74b47bf3 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/fine_tunes.go @@ -0,0 +1,178 @@ +package openai + +import ( + "context" + "fmt" + "net/http" +) + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +type FineTuneRequest struct { + TrainingFile string `json:"training_file"` + ValidationFile string `json:"validation_file,omitempty"` + Model string `json:"model,omitempty"` + Epochs int `json:"n_epochs,omitempty"` + BatchSize int `json:"batch_size,omitempty"` + LearningRateMultiplier float32 `json:"learning_rate_multiplier,omitempty"` + PromptLossRate float32 `json:"prompt_loss_rate,omitempty"` + ComputeClassificationMetrics bool `json:"compute_classification_metrics,omitempty"` + ClassificationClasses int `json:"classification_n_classes,omitempty"` + ClassificationPositiveClass string `json:"classification_positive_class,omitempty"` + ClassificationBetas []float32 `json:"classification_betas,omitempty"` + Suffix string `json:"suffix,omitempty"` +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +type FineTune struct { + ID string `json:"id"` + Object string `json:"object"` + Model string `json:"model"` + CreatedAt int64 `json:"created_at"` + FineTuneEventList []FineTuneEvent `json:"events,omitempty"` + FineTunedModel string `json:"fine_tuned_model"` + HyperParams FineTuneHyperParams `json:"hyperparams"` + OrganizationID string `json:"organization_id"` + ResultFiles []File `json:"result_files"` + Status string `json:"status"` + ValidationFiles []File `json:"validation_files"` + TrainingFiles []File `json:"training_files"` + UpdatedAt int64 `json:"updated_at"` + + httpHeader +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +type FineTuneEvent struct { + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + Level string `json:"level"` + Message string `json:"message"` +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +type FineTuneHyperParams struct { + BatchSize int `json:"batch_size"` + LearningRateMultiplier float64 `json:"learning_rate_multiplier"` + Epochs int `json:"n_epochs"` + PromptLossWeight float64 `json:"prompt_loss_weight"` +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +type FineTuneList struct { + Object string `json:"object"` + Data []FineTune `json:"data"` + + httpHeader +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +type FineTuneEventList struct { + Object string `json:"object"` + Data []FineTuneEvent `json:"data"` + + httpHeader +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +type FineTuneDeleteResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + + httpHeader +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +func (c *Client) CreateFineTune(ctx context.Context, request FineTuneRequest) (response FineTune, err error) { + urlSuffix := "/fine-tunes" + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CancelFineTune cancel a fine-tune job. +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +func (c *Client) CancelFineTune(ctx context.Context, fineTuneID string) (response FineTune, err error) { + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/fine-tunes/"+fineTuneID+"/cancel")) //nolint:lll //this method is deprecated + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +func (c *Client) ListFineTunes(ctx context.Context) (response FineTuneList, err error) { + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/fine-tunes")) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +func (c *Client) GetFineTune(ctx context.Context, fineTuneID string) (response FineTune, err error) { + urlSuffix := fmt.Sprintf("/fine-tunes/%s", fineTuneID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +func (c *Client) DeleteFineTune(ctx context.Context, fineTuneID string) (response FineTuneDeleteResponse, err error) { + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL("/fine-tunes/"+fineTuneID)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API. +// This API will be officially deprecated on January 4th, 2024. +// OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go. +func (c *Client) ListFineTuneEvents(ctx context.Context, fineTuneID string) (response FineTuneEventList, err error) { + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/fine-tunes/"+fineTuneID+"/events")) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/fine_tuning_job.go b/vendor/github.com/sashabaranov/go-openai/fine_tuning_job.go new file mode 100644 index 00000000..5a9f54a9 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/fine_tuning_job.go @@ -0,0 +1,159 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +type FineTuningJob struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + FinishedAt int64 `json:"finished_at"` + Model string `json:"model"` + FineTunedModel string `json:"fine_tuned_model,omitempty"` + OrganizationID string `json:"organization_id"` + Status string `json:"status"` + Hyperparameters Hyperparameters `json:"hyperparameters"` + TrainingFile string `json:"training_file"` + ValidationFile string `json:"validation_file,omitempty"` + ResultFiles []string `json:"result_files"` + TrainedTokens int `json:"trained_tokens"` + + httpHeader +} + +type Hyperparameters struct { + Epochs any `json:"n_epochs,omitempty"` + LearningRateMultiplier any `json:"learning_rate_multiplier,omitempty"` + BatchSize any `json:"batch_size,omitempty"` +} + +type FineTuningJobRequest struct { + TrainingFile string `json:"training_file"` + ValidationFile string `json:"validation_file,omitempty"` + Model string `json:"model,omitempty"` + Hyperparameters *Hyperparameters `json:"hyperparameters,omitempty"` + Suffix string `json:"suffix,omitempty"` +} + +type FineTuningJobEventList struct { + Object string `json:"object"` + Data []FineTuneEvent `json:"data"` + HasMore bool `json:"has_more"` + + httpHeader +} + +type FineTuningJobEvent struct { + Object string `json:"object"` + ID string `json:"id"` + CreatedAt int `json:"created_at"` + Level string `json:"level"` + Message string `json:"message"` + Data any `json:"data"` + Type string `json:"type"` +} + +// CreateFineTuningJob create a fine tuning job. +func (c *Client) CreateFineTuningJob( + ctx context.Context, + request FineTuningJobRequest, +) (response FineTuningJob, err error) { + urlSuffix := "/fine_tuning/jobs" + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CancelFineTuningJob cancel a fine tuning job. +func (c *Client) CancelFineTuningJob(ctx context.Context, fineTuningJobID string) (response FineTuningJob, err error) { + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/fine_tuning/jobs/"+fineTuningJobID+"/cancel")) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveFineTuningJob retrieve a fine tuning job. +func (c *Client) RetrieveFineTuningJob( + ctx context.Context, + fineTuningJobID string, +) (response FineTuningJob, err error) { + urlSuffix := fmt.Sprintf("/fine_tuning/jobs/%s", fineTuningJobID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +type listFineTuningJobEventsParameters struct { + after *string + limit *int +} + +type ListFineTuningJobEventsParameter func(*listFineTuningJobEventsParameters) + +func ListFineTuningJobEventsWithAfter(after string) ListFineTuningJobEventsParameter { + return func(args *listFineTuningJobEventsParameters) { + args.after = &after + } +} + +func ListFineTuningJobEventsWithLimit(limit int) ListFineTuningJobEventsParameter { + return func(args *listFineTuningJobEventsParameters) { + args.limit = &limit + } +} + +// ListFineTuningJobs list fine tuning jobs events. +func (c *Client) ListFineTuningJobEvents( + ctx context.Context, + fineTuningJobID string, + setters ...ListFineTuningJobEventsParameter, +) (response FineTuningJobEventList, err error) { + parameters := &listFineTuningJobEventsParameters{ + after: nil, + limit: nil, + } + + for _, setter := range setters { + setter(parameters) + } + + urlValues := url.Values{} + if parameters.after != nil { + urlValues.Add("after", *parameters.after) + } + if parameters.limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *parameters.limit)) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + req, err := c.newRequest( + ctx, + http.MethodGet, + c.fullURL("/fine_tuning/jobs/"+fineTuningJobID+"/events"+encodedValues), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/image.go b/vendor/github.com/sashabaranov/go-openai/image.go new file mode 100644 index 00000000..577d7db9 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/image.go @@ -0,0 +1,209 @@ +package openai + +import ( + "bytes" + "context" + "net/http" + "os" + "strconv" +) + +// Image sizes defined by the OpenAI API. +const ( + CreateImageSize256x256 = "256x256" + CreateImageSize512x512 = "512x512" + CreateImageSize1024x1024 = "1024x1024" + // dall-e-3 supported only. + CreateImageSize1792x1024 = "1792x1024" + CreateImageSize1024x1792 = "1024x1792" +) + +const ( + CreateImageResponseFormatURL = "url" + CreateImageResponseFormatB64JSON = "b64_json" +) + +const ( + CreateImageModelDallE2 = "dall-e-2" + CreateImageModelDallE3 = "dall-e-3" +) + +const ( + CreateImageQualityHD = "hd" + CreateImageQualityStandard = "standard" +) + +const ( + CreateImageStyleVivid = "vivid" + CreateImageStyleNatural = "natural" +) + +// ImageRequest represents the request structure for the image API. +type ImageRequest struct { + Prompt string `json:"prompt,omitempty"` + Model string `json:"model,omitempty"` + N int `json:"n,omitempty"` + Quality string `json:"quality,omitempty"` + Size string `json:"size,omitempty"` + Style string `json:"style,omitempty"` + ResponseFormat string `json:"response_format,omitempty"` + User string `json:"user,omitempty"` +} + +// ImageResponse represents a response structure for image API. +type ImageResponse struct { + Created int64 `json:"created,omitempty"` + Data []ImageResponseDataInner `json:"data,omitempty"` + + httpHeader +} + +// ImageResponseDataInner represents a response data structure for image API. +type ImageResponseDataInner struct { + URL string `json:"url,omitempty"` + B64JSON string `json:"b64_json,omitempty"` + RevisedPrompt string `json:"revised_prompt,omitempty"` +} + +// CreateImage - API call to create an image. This is the main endpoint of the DALL-E API. +func (c *Client) CreateImage(ctx context.Context, request ImageRequest) (response ImageResponse, err error) { + urlSuffix := "/images/generations" + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix, withModel(request.Model)), + withBody(request), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ImageEditRequest represents the request structure for the image API. +type ImageEditRequest struct { + Image *os.File `json:"image,omitempty"` + Mask *os.File `json:"mask,omitempty"` + Prompt string `json:"prompt,omitempty"` + Model string `json:"model,omitempty"` + N int `json:"n,omitempty"` + Size string `json:"size,omitempty"` + ResponseFormat string `json:"response_format,omitempty"` +} + +// CreateEditImage - API call to create an image. This is the main endpoint of the DALL-E API. +func (c *Client) CreateEditImage(ctx context.Context, request ImageEditRequest) (response ImageResponse, err error) { + body := &bytes.Buffer{} + builder := c.createFormBuilder(body) + + // image + err = builder.CreateFormFile("image", request.Image) + if err != nil { + return + } + + // mask, it is optional + if request.Mask != nil { + err = builder.CreateFormFile("mask", request.Mask) + if err != nil { + return + } + } + + err = builder.WriteField("prompt", request.Prompt) + if err != nil { + return + } + + err = builder.WriteField("n", strconv.Itoa(request.N)) + if err != nil { + return + } + + err = builder.WriteField("size", request.Size) + if err != nil { + return + } + + err = builder.WriteField("response_format", request.ResponseFormat) + if err != nil { + return + } + + err = builder.Close() + if err != nil { + return + } + + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL("/images/edits", withModel(request.Model)), + withBody(body), + withContentType(builder.FormDataContentType()), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ImageVariRequest represents the request structure for the image API. +type ImageVariRequest struct { + Image *os.File `json:"image,omitempty"` + Model string `json:"model,omitempty"` + N int `json:"n,omitempty"` + Size string `json:"size,omitempty"` + ResponseFormat string `json:"response_format,omitempty"` +} + +// CreateVariImage - API call to create an image variation. This is the main endpoint of the DALL-E API. +// Use abbreviations(vari for variation) because ci-lint has a single-line length limit ... +func (c *Client) CreateVariImage(ctx context.Context, request ImageVariRequest) (response ImageResponse, err error) { + body := &bytes.Buffer{} + builder := c.createFormBuilder(body) + + // image + err = builder.CreateFormFile("image", request.Image) + if err != nil { + return + } + + err = builder.WriteField("n", strconv.Itoa(request.N)) + if err != nil { + return + } + + err = builder.WriteField("size", request.Size) + if err != nil { + return + } + + err = builder.WriteField("response_format", request.ResponseFormat) + if err != nil { + return + } + + err = builder.Close() + if err != nil { + return + } + + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL("/images/variations", withModel(request.Model)), + withBody(body), + withContentType(builder.FormDataContentType()), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/internal/error_accumulator.go b/vendor/github.com/sashabaranov/go-openai/internal/error_accumulator.go new file mode 100644 index 00000000..3d3e805f --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/internal/error_accumulator.go @@ -0,0 +1,44 @@ +package openai + +import ( + "bytes" + "fmt" + "io" +) + +type ErrorAccumulator interface { + Write(p []byte) error + Bytes() []byte +} + +type errorBuffer interface { + io.Writer + Len() int + Bytes() []byte +} + +type DefaultErrorAccumulator struct { + Buffer errorBuffer +} + +func NewErrorAccumulator() ErrorAccumulator { + return &DefaultErrorAccumulator{ + Buffer: &bytes.Buffer{}, + } +} + +func (e *DefaultErrorAccumulator) Write(p []byte) error { + _, err := e.Buffer.Write(p) + if err != nil { + return fmt.Errorf("error accumulator write error, %w", err) + } + return nil +} + +func (e *DefaultErrorAccumulator) Bytes() (errBytes []byte) { + if e.Buffer.Len() == 0 { + return + } + errBytes = e.Buffer.Bytes() + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/internal/form_builder.go b/vendor/github.com/sashabaranov/go-openai/internal/form_builder.go new file mode 100644 index 00000000..2224fad4 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/internal/form_builder.go @@ -0,0 +1,65 @@ +package openai + +import ( + "fmt" + "io" + "mime/multipart" + "os" + "path" +) + +type FormBuilder interface { + CreateFormFile(fieldname string, file *os.File) error + CreateFormFileReader(fieldname string, r io.Reader, filename string) error + WriteField(fieldname, value string) error + Close() error + FormDataContentType() string +} + +type DefaultFormBuilder struct { + writer *multipart.Writer +} + +func NewFormBuilder(body io.Writer) *DefaultFormBuilder { + return &DefaultFormBuilder{ + writer: multipart.NewWriter(body), + } +} + +func (fb *DefaultFormBuilder) CreateFormFile(fieldname string, file *os.File) error { + return fb.createFormFile(fieldname, file, file.Name()) +} + +func (fb *DefaultFormBuilder) CreateFormFileReader(fieldname string, r io.Reader, filename string) error { + return fb.createFormFile(fieldname, r, path.Base(filename)) +} + +func (fb *DefaultFormBuilder) createFormFile(fieldname string, r io.Reader, filename string) error { + if filename == "" { + return fmt.Errorf("filename cannot be empty") + } + + fieldWriter, err := fb.writer.CreateFormFile(fieldname, filename) + if err != nil { + return err + } + + _, err = io.Copy(fieldWriter, r) + if err != nil { + return err + } + + return nil +} + +func (fb *DefaultFormBuilder) WriteField(fieldname, value string) error { + return fb.writer.WriteField(fieldname, value) +} + +func (fb *DefaultFormBuilder) Close() error { + return fb.writer.Close() +} + +func (fb *DefaultFormBuilder) FormDataContentType() string { + return fb.writer.FormDataContentType() +} diff --git a/vendor/github.com/sashabaranov/go-openai/internal/marshaller.go b/vendor/github.com/sashabaranov/go-openai/internal/marshaller.go new file mode 100644 index 00000000..223a4dc1 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/internal/marshaller.go @@ -0,0 +1,15 @@ +package openai + +import ( + "encoding/json" +) + +type Marshaller interface { + Marshal(value any) ([]byte, error) +} + +type JSONMarshaller struct{} + +func (jm *JSONMarshaller) Marshal(value any) ([]byte, error) { + return json.Marshal(value) +} diff --git a/vendor/github.com/sashabaranov/go-openai/internal/request_builder.go b/vendor/github.com/sashabaranov/go-openai/internal/request_builder.go new file mode 100644 index 00000000..5699f6b1 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/internal/request_builder.go @@ -0,0 +1,52 @@ +package openai + +import ( + "bytes" + "context" + "io" + "net/http" +) + +type RequestBuilder interface { + Build(ctx context.Context, method, url string, body any, header http.Header) (*http.Request, error) +} + +type HTTPRequestBuilder struct { + marshaller Marshaller +} + +func NewRequestBuilder() *HTTPRequestBuilder { + return &HTTPRequestBuilder{ + marshaller: &JSONMarshaller{}, + } +} + +func (b *HTTPRequestBuilder) Build( + ctx context.Context, + method string, + url string, + body any, + header http.Header, +) (req *http.Request, err error) { + var bodyReader io.Reader + if body != nil { + if v, ok := body.(io.Reader); ok { + bodyReader = v + } else { + var reqBytes []byte + reqBytes, err = b.marshaller.Marshal(body) + if err != nil { + return + } + bodyReader = bytes.NewBuffer(reqBytes) + } + } + req, err = http.NewRequestWithContext(ctx, method, url, bodyReader) + if err != nil { + return + } + if header != nil { + req.Header = header + } + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/internal/unmarshaler.go b/vendor/github.com/sashabaranov/go-openai/internal/unmarshaler.go new file mode 100644 index 00000000..88287602 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/internal/unmarshaler.go @@ -0,0 +1,15 @@ +package openai + +import ( + "encoding/json" +) + +type Unmarshaler interface { + Unmarshal(data []byte, v any) error +} + +type JSONUnmarshaler struct{} + +func (jm *JSONUnmarshaler) Unmarshal(data []byte, v any) error { + return json.Unmarshal(data, v) +} diff --git a/vendor/github.com/sashabaranov/go-openai/messages.go b/vendor/github.com/sashabaranov/go-openai/messages.go new file mode 100644 index 00000000..90236393 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/messages.go @@ -0,0 +1,218 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +const ( + messagesSuffix = "messages" +) + +type Message struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int `json:"created_at"` + ThreadID string `json:"thread_id"` + Role string `json:"role"` + Content []MessageContent `json:"content"` + FileIds []string `json:"file_ids"` //nolint:revive //backwards-compatibility + AssistantID *string `json:"assistant_id,omitempty"` + RunID *string `json:"run_id,omitempty"` + Metadata map[string]any `json:"metadata"` + + httpHeader +} + +type MessagesList struct { + Messages []Message `json:"data"` + + Object string `json:"object"` + FirstID *string `json:"first_id"` + LastID *string `json:"last_id"` + HasMore bool `json:"has_more"` + + httpHeader +} + +type MessageContent struct { + Type string `json:"type"` + Text *MessageText `json:"text,omitempty"` + ImageFile *ImageFile `json:"image_file,omitempty"` +} +type MessageText struct { + Value string `json:"value"` + Annotations []any `json:"annotations"` +} + +type ImageFile struct { + FileID string `json:"file_id"` +} + +type MessageRequest struct { + Role string `json:"role"` + Content string `json:"content"` + FileIds []string `json:"file_ids,omitempty"` //nolint:revive // backwards-compatibility + Metadata map[string]any `json:"metadata,omitempty"` + Attachments []ThreadAttachment `json:"attachments,omitempty"` +} + +type MessageFile struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int `json:"created_at"` + MessageID string `json:"message_id"` + + httpHeader +} + +type MessageFilesList struct { + MessageFiles []MessageFile `json:"data"` + + httpHeader +} + +type MessageDeletionStatus struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + + httpHeader +} + +// CreateMessage creates a new message. +func (c *Client) CreateMessage(ctx context.Context, threadID string, request MessageRequest) (msg Message, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/%s", threadID, messagesSuffix) + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &msg) + return +} + +// ListMessage fetches all messages in the thread. +func (c *Client) ListMessage(ctx context.Context, threadID string, + limit *int, + order *string, + after *string, + before *string, + runID *string, +) (messages MessagesList, err error) { + urlValues := url.Values{} + if limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *limit)) + } + if order != nil { + urlValues.Add("order", *order) + } + if after != nil { + urlValues.Add("after", *after) + } + if before != nil { + urlValues.Add("before", *before) + } + if runID != nil { + urlValues.Add("run_id", *runID) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("/threads/%s/%s%s", threadID, messagesSuffix, encodedValues) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &messages) + return +} + +// RetrieveMessage retrieves a Message. +func (c *Client) RetrieveMessage( + ctx context.Context, + threadID, messageID string, +) (msg Message, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/%s/%s", threadID, messagesSuffix, messageID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &msg) + return +} + +// ModifyMessage modifies a message. +func (c *Client) ModifyMessage( + ctx context.Context, + threadID, messageID string, + metadata map[string]string, +) (msg Message, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/%s/%s", threadID, messagesSuffix, messageID) + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), + withBody(map[string]any{"metadata": metadata}), withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &msg) + return +} + +// RetrieveMessageFile fetches a message file. +func (c *Client) RetrieveMessageFile( + ctx context.Context, + threadID, messageID, fileID string, +) (file MessageFile, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/%s/%s/files/%s", threadID, messagesSuffix, messageID, fileID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &file) + return +} + +// ListMessageFiles fetches all files attached to a message. +func (c *Client) ListMessageFiles( + ctx context.Context, + threadID, messageID string, +) (files MessageFilesList, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/%s/%s/files", threadID, messagesSuffix, messageID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &files) + return +} + +// DeleteMessage deletes a message.. +func (c *Client) DeleteMessage( + ctx context.Context, + threadID, messageID string, +) (status MessageDeletionStatus, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/%s/%s", threadID, messagesSuffix, messageID) + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &status) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/models.go b/vendor/github.com/sashabaranov/go-openai/models.go new file mode 100644 index 00000000..d94f9883 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/models.go @@ -0,0 +1,90 @@ +package openai + +import ( + "context" + "fmt" + "net/http" +) + +// Model struct represents an OpenAPI model. +type Model struct { + CreatedAt int64 `json:"created"` + ID string `json:"id"` + Object string `json:"object"` + OwnedBy string `json:"owned_by"` + Permission []Permission `json:"permission"` + Root string `json:"root"` + Parent string `json:"parent"` + + httpHeader +} + +// Permission struct represents an OpenAPI permission. +type Permission struct { + CreatedAt int64 `json:"created"` + ID string `json:"id"` + Object string `json:"object"` + AllowCreateEngine bool `json:"allow_create_engine"` + AllowSampling bool `json:"allow_sampling"` + AllowLogprobs bool `json:"allow_logprobs"` + AllowSearchIndices bool `json:"allow_search_indices"` + AllowView bool `json:"allow_view"` + AllowFineTuning bool `json:"allow_fine_tuning"` + Organization string `json:"organization"` + Group interface{} `json:"group"` + IsBlocking bool `json:"is_blocking"` +} + +// FineTuneModelDeleteResponse represents the deletion status of a fine-tuned model. +type FineTuneModelDeleteResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + + httpHeader +} + +// ModelsList is a list of models, including those that belong to the user or organization. +type ModelsList struct { + Models []Model `json:"data"` + + httpHeader +} + +// ListModels Lists the currently available models, +// and provides basic information about each model such as the model id and parent. +func (c *Client) ListModels(ctx context.Context) (models ModelsList, err error) { + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/models")) + if err != nil { + return + } + + err = c.sendRequest(req, &models) + return +} + +// GetModel Retrieves a model instance, providing basic information about +// the model such as the owner and permissioning. +func (c *Client) GetModel(ctx context.Context, modelID string) (model Model, err error) { + urlSuffix := fmt.Sprintf("/models/%s", modelID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &model) + return +} + +// DeleteFineTuneModel Deletes a fine-tune model. You must have the Owner +// role in your organization to delete a model. +func (c *Client) DeleteFineTuneModel(ctx context.Context, modelID string) ( + response FineTuneModelDeleteResponse, err error) { + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL("/models/"+modelID)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/moderation.go b/vendor/github.com/sashabaranov/go-openai/moderation.go new file mode 100644 index 00000000..a0e09c0e --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/moderation.go @@ -0,0 +1,107 @@ +package openai + +import ( + "context" + "errors" + "net/http" +) + +// The moderation endpoint is a tool you can use to check whether content complies with OpenAI's usage policies. +// Developers can thus identify content that our usage policies prohibits and take action, for instance by filtering it. + +// The default is text-moderation-latest which will be automatically upgraded over time. +// This ensures you are always using our most accurate model. +// If you use text-moderation-stable, we will provide advanced notice before updating the model. +// Accuracy of text-moderation-stable may be slightly lower than for text-moderation-latest. +const ( + ModerationOmniLatest = "omni-moderation-latest" + ModerationOmni20240926 = "omni-moderation-2024-09-26" + ModerationTextStable = "text-moderation-stable" + ModerationTextLatest = "text-moderation-latest" + // Deprecated: use ModerationTextStable and ModerationTextLatest instead. + ModerationText001 = "text-moderation-001" +) + +var ( + ErrModerationInvalidModel = errors.New("this model is not supported with moderation, please use text-moderation-stable or text-moderation-latest instead") //nolint:lll +) + +var validModerationModel = map[string]struct{}{ + ModerationOmniLatest: {}, + ModerationOmni20240926: {}, + ModerationTextStable: {}, + ModerationTextLatest: {}, +} + +// ModerationRequest represents a request structure for moderation API. +type ModerationRequest struct { + Input string `json:"input,omitempty"` + Model string `json:"model,omitempty"` +} + +// Result represents one of possible moderation results. +type Result struct { + Categories ResultCategories `json:"categories"` + CategoryScores ResultCategoryScores `json:"category_scores"` + Flagged bool `json:"flagged"` +} + +// ResultCategories represents Categories of Result. +type ResultCategories struct { + Hate bool `json:"hate"` + HateThreatening bool `json:"hate/threatening"` + Harassment bool `json:"harassment"` + HarassmentThreatening bool `json:"harassment/threatening"` + SelfHarm bool `json:"self-harm"` + SelfHarmIntent bool `json:"self-harm/intent"` + SelfHarmInstructions bool `json:"self-harm/instructions"` + Sexual bool `json:"sexual"` + SexualMinors bool `json:"sexual/minors"` + Violence bool `json:"violence"` + ViolenceGraphic bool `json:"violence/graphic"` +} + +// ResultCategoryScores represents CategoryScores of Result. +type ResultCategoryScores struct { + Hate float32 `json:"hate"` + HateThreatening float32 `json:"hate/threatening"` + Harassment float32 `json:"harassment"` + HarassmentThreatening float32 `json:"harassment/threatening"` + SelfHarm float32 `json:"self-harm"` + SelfHarmIntent float32 `json:"self-harm/intent"` + SelfHarmInstructions float32 `json:"self-harm/instructions"` + Sexual float32 `json:"sexual"` + SexualMinors float32 `json:"sexual/minors"` + Violence float32 `json:"violence"` + ViolenceGraphic float32 `json:"violence/graphic"` +} + +// ModerationResponse represents a response structure for moderation API. +type ModerationResponse struct { + ID string `json:"id"` + Model string `json:"model"` + Results []Result `json:"results"` + + httpHeader +} + +// Moderations — perform a moderation api call over a string. +// Input can be an array or slice but a string will reduce the complexity. +func (c *Client) Moderations(ctx context.Context, request ModerationRequest) (response ModerationResponse, err error) { + if _, ok := validModerationModel[request.Model]; len(request.Model) > 0 && !ok { + err = ErrModerationInvalidModel + return + } + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL("/moderations", withModel(request.Model)), + withBody(&request), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/ratelimit.go b/vendor/github.com/sashabaranov/go-openai/ratelimit.go new file mode 100644 index 00000000..e8953f71 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/ratelimit.go @@ -0,0 +1,43 @@ +package openai + +import ( + "net/http" + "strconv" + "time" +) + +// RateLimitHeaders struct represents Openai rate limits headers. +type RateLimitHeaders struct { + LimitRequests int `json:"x-ratelimit-limit-requests"` + LimitTokens int `json:"x-ratelimit-limit-tokens"` + RemainingRequests int `json:"x-ratelimit-remaining-requests"` + RemainingTokens int `json:"x-ratelimit-remaining-tokens"` + ResetRequests ResetTime `json:"x-ratelimit-reset-requests"` + ResetTokens ResetTime `json:"x-ratelimit-reset-tokens"` +} + +type ResetTime string + +func (r ResetTime) String() string { + return string(r) +} + +func (r ResetTime) Time() time.Time { + d, _ := time.ParseDuration(string(r)) + return time.Now().Add(d) +} + +func newRateLimitHeaders(h http.Header) RateLimitHeaders { + limitReq, _ := strconv.Atoi(h.Get("x-ratelimit-limit-requests")) + limitTokens, _ := strconv.Atoi(h.Get("x-ratelimit-limit-tokens")) + remainingReq, _ := strconv.Atoi(h.Get("x-ratelimit-remaining-requests")) + remainingTokens, _ := strconv.Atoi(h.Get("x-ratelimit-remaining-tokens")) + return RateLimitHeaders{ + LimitRequests: limitReq, + LimitTokens: limitTokens, + RemainingRequests: remainingReq, + RemainingTokens: remainingTokens, + ResetRequests: ResetTime(h.Get("x-ratelimit-reset-requests")), + ResetTokens: ResetTime(h.Get("x-ratelimit-reset-tokens")), + } +} diff --git a/vendor/github.com/sashabaranov/go-openai/run.go b/vendor/github.com/sashabaranov/go-openai/run.go new file mode 100644 index 00000000..d3e755f0 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/run.go @@ -0,0 +1,453 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +type Run struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + ThreadID string `json:"thread_id"` + AssistantID string `json:"assistant_id"` + Status RunStatus `json:"status"` + RequiredAction *RunRequiredAction `json:"required_action,omitempty"` + LastError *RunLastError `json:"last_error,omitempty"` + ExpiresAt int64 `json:"expires_at"` + StartedAt *int64 `json:"started_at,omitempty"` + CancelledAt *int64 `json:"cancelled_at,omitempty"` + FailedAt *int64 `json:"failed_at,omitempty"` + CompletedAt *int64 `json:"completed_at,omitempty"` + Model string `json:"model"` + Instructions string `json:"instructions,omitempty"` + Tools []Tool `json:"tools"` + FileIDS []string `json:"file_ids"` //nolint:revive // backwards-compatibility + Metadata map[string]any `json:"metadata"` + Usage Usage `json:"usage,omitempty"` + + Temperature *float32 `json:"temperature,omitempty"` + // The maximum number of prompt tokens that may be used over the course of the run. + // If the run exceeds the number of prompt tokens specified, the run will end with status 'incomplete'. + MaxPromptTokens int `json:"max_prompt_tokens,omitempty"` + // The maximum number of completion tokens that may be used over the course of the run. + // If the run exceeds the number of completion tokens specified, the run will end with status 'incomplete'. + MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` + // ThreadTruncationStrategy defines the truncation strategy to use for the thread. + TruncationStrategy *ThreadTruncationStrategy `json:"truncation_strategy,omitempty"` + + httpHeader +} + +type RunStatus string + +const ( + RunStatusQueued RunStatus = "queued" + RunStatusInProgress RunStatus = "in_progress" + RunStatusRequiresAction RunStatus = "requires_action" + RunStatusCancelling RunStatus = "cancelling" + RunStatusFailed RunStatus = "failed" + RunStatusCompleted RunStatus = "completed" + RunStatusIncomplete RunStatus = "incomplete" + RunStatusExpired RunStatus = "expired" + RunStatusCancelled RunStatus = "cancelled" +) + +type RunRequiredAction struct { + Type RequiredActionType `json:"type"` + SubmitToolOutputs *SubmitToolOutputs `json:"submit_tool_outputs,omitempty"` +} + +type RequiredActionType string + +const ( + RequiredActionTypeSubmitToolOutputs RequiredActionType = "submit_tool_outputs" +) + +type SubmitToolOutputs struct { + ToolCalls []ToolCall `json:"tool_calls"` +} + +type RunLastError struct { + Code RunError `json:"code"` + Message string `json:"message"` +} + +type RunError string + +const ( + RunErrorServerError RunError = "server_error" + RunErrorRateLimitExceeded RunError = "rate_limit_exceeded" +) + +type RunRequest struct { + AssistantID string `json:"assistant_id"` + Model string `json:"model,omitempty"` + Instructions string `json:"instructions,omitempty"` + AdditionalInstructions string `json:"additional_instructions,omitempty"` + Tools []Tool `json:"tools,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` + + // Sampling temperature between 0 and 2. Higher values like 0.8 are more random. + // lower values are more focused and deterministic. + Temperature *float32 `json:"temperature,omitempty"` + TopP *float32 `json:"top_p,omitempty"` + + // The maximum number of prompt tokens that may be used over the course of the run. + // If the run exceeds the number of prompt tokens specified, the run will end with status 'incomplete'. + MaxPromptTokens int `json:"max_prompt_tokens,omitempty"` + + // The maximum number of completion tokens that may be used over the course of the run. + // If the run exceeds the number of completion tokens specified, the run will end with status 'incomplete'. + MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` + + // ThreadTruncationStrategy defines the truncation strategy to use for the thread. + TruncationStrategy *ThreadTruncationStrategy `json:"truncation_strategy,omitempty"` + + // This can be either a string or a ToolChoice object. + ToolChoice any `json:"tool_choice,omitempty"` + // This can be either a string or a ResponseFormat object. + ResponseFormat any `json:"response_format,omitempty"` + // Disable the default behavior of parallel tool calls by setting it: false. + ParallelToolCalls any `json:"parallel_tool_calls,omitempty"` +} + +// ThreadTruncationStrategy defines the truncation strategy to use for the thread. +// https://platform.openai.com/docs/assistants/how-it-works/truncation-strategy. +type ThreadTruncationStrategy struct { + // default 'auto'. + Type TruncationStrategy `json:"type,omitempty"` + // this field should be set if the truncation strategy is set to LastMessages. + LastMessages *int `json:"last_messages,omitempty"` +} + +// TruncationStrategy defines the existing truncation strategies existing for thread management in an assistant. +type TruncationStrategy string + +const ( + // TruncationStrategyAuto messages in the middle of the thread will be dropped to fit the context length of the model. + TruncationStrategyAuto = TruncationStrategy("auto") + // TruncationStrategyLastMessages the thread will be truncated to the n most recent messages in the thread. + TruncationStrategyLastMessages = TruncationStrategy("last_messages") +) + +// ReponseFormat specifies the format the model must output. +// https://platform.openai.com/docs/api-reference/runs/createRun#runs-createrun-response_format. +// Type can either be text or json_object. +type ReponseFormat struct { + Type string `json:"type"` +} + +type RunModifyRequest struct { + Metadata map[string]any `json:"metadata,omitempty"` +} + +// RunList is a list of runs. +type RunList struct { + Runs []Run `json:"data"` + + httpHeader +} + +type SubmitToolOutputsRequest struct { + ToolOutputs []ToolOutput `json:"tool_outputs"` +} + +type ToolOutput struct { + ToolCallID string `json:"tool_call_id"` + Output any `json:"output"` +} + +type CreateThreadAndRunRequest struct { + RunRequest + Thread ThreadRequest `json:"thread"` +} + +type RunStep struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + AssistantID string `json:"assistant_id"` + ThreadID string `json:"thread_id"` + RunID string `json:"run_id"` + Type RunStepType `json:"type"` + Status RunStepStatus `json:"status"` + StepDetails StepDetails `json:"step_details"` + LastError *RunLastError `json:"last_error,omitempty"` + ExpiredAt *int64 `json:"expired_at,omitempty"` + CancelledAt *int64 `json:"cancelled_at,omitempty"` + FailedAt *int64 `json:"failed_at,omitempty"` + CompletedAt *int64 `json:"completed_at,omitempty"` + Metadata map[string]any `json:"metadata"` + + httpHeader +} + +type RunStepStatus string + +const ( + RunStepStatusInProgress RunStepStatus = "in_progress" + RunStepStatusCancelling RunStepStatus = "cancelled" + RunStepStatusFailed RunStepStatus = "failed" + RunStepStatusCompleted RunStepStatus = "completed" + RunStepStatusExpired RunStepStatus = "expired" +) + +type RunStepType string + +const ( + RunStepTypeMessageCreation RunStepType = "message_creation" + RunStepTypeToolCalls RunStepType = "tool_calls" +) + +type StepDetails struct { + Type RunStepType `json:"type"` + MessageCreation *StepDetailsMessageCreation `json:"message_creation,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` +} + +type StepDetailsMessageCreation struct { + MessageID string `json:"message_id"` +} + +// RunStepList is a list of steps. +type RunStepList struct { + RunSteps []RunStep `json:"data"` + + FirstID string `json:"first_id"` + LastID string `json:"last_id"` + HasMore bool `json:"has_more"` + + httpHeader +} + +type Pagination struct { + Limit *int + Order *string + After *string + Before *string +} + +// CreateRun creates a new run. +func (c *Client) CreateRun( + ctx context.Context, + threadID string, + request RunRequest, +) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs", threadID) + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveRun retrieves a run. +func (c *Client) RetrieveRun( + ctx context.Context, + threadID string, + runID string, +) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s", threadID, runID) + req, err := c.newRequest( + ctx, + http.MethodGet, + c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ModifyRun modifies a run. +func (c *Client) ModifyRun( + ctx context.Context, + threadID string, + runID string, + request RunModifyRequest, +) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s", threadID, runID) + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ListRuns lists runs. +func (c *Client) ListRuns( + ctx context.Context, + threadID string, + pagination Pagination, +) (response RunList, err error) { + urlValues := url.Values{} + if pagination.Limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *pagination.Limit)) + } + if pagination.Order != nil { + urlValues.Add("order", *pagination.Order) + } + if pagination.After != nil { + urlValues.Add("after", *pagination.After) + } + if pagination.Before != nil { + urlValues.Add("before", *pagination.Before) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("/threads/%s/runs%s", threadID, encodedValues) + req, err := c.newRequest( + ctx, + http.MethodGet, + c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// SubmitToolOutputs submits tool outputs. +func (c *Client) SubmitToolOutputs( + ctx context.Context, + threadID string, + runID string, + request SubmitToolOutputsRequest) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/submit_tool_outputs", threadID, runID) + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CancelRun cancels a run. +func (c *Client) CancelRun( + ctx context.Context, + threadID string, + runID string) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/cancel", threadID, runID) + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CreateThreadAndRun submits tool outputs. +func (c *Client) CreateThreadAndRun( + ctx context.Context, + request CreateThreadAndRunRequest) (response Run, err error) { + urlSuffix := "/threads/runs" + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveRunStep retrieves a run step. +func (c *Client) RetrieveRunStep( + ctx context.Context, + threadID string, + runID string, + stepID string, +) (response RunStep, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/steps/%s", threadID, runID, stepID) + req, err := c.newRequest( + ctx, + http.MethodGet, + c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ListRunSteps lists run steps. +func (c *Client) ListRunSteps( + ctx context.Context, + threadID string, + runID string, + pagination Pagination, +) (response RunStepList, err error) { + urlValues := url.Values{} + if pagination.Limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *pagination.Limit)) + } + if pagination.Order != nil { + urlValues.Add("order", *pagination.Order) + } + if pagination.After != nil { + urlValues.Add("after", *pagination.After) + } + if pagination.Before != nil { + urlValues.Add("before", *pagination.Before) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/steps%s", threadID, runID, encodedValues) + req, err := c.newRequest( + ctx, + http.MethodGet, + c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/speech.go b/vendor/github.com/sashabaranov/go-openai/speech.go new file mode 100644 index 00000000..20b52e33 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/speech.go @@ -0,0 +1,59 @@ +package openai + +import ( + "context" + "net/http" +) + +type SpeechModel string + +const ( + TTSModel1 SpeechModel = "tts-1" + TTSModel1HD SpeechModel = "tts-1-hd" + TTSModelCanary SpeechModel = "canary-tts" +) + +type SpeechVoice string + +const ( + VoiceAlloy SpeechVoice = "alloy" + VoiceEcho SpeechVoice = "echo" + VoiceFable SpeechVoice = "fable" + VoiceOnyx SpeechVoice = "onyx" + VoiceNova SpeechVoice = "nova" + VoiceShimmer SpeechVoice = "shimmer" +) + +type SpeechResponseFormat string + +const ( + SpeechResponseFormatMp3 SpeechResponseFormat = "mp3" + SpeechResponseFormatOpus SpeechResponseFormat = "opus" + SpeechResponseFormatAac SpeechResponseFormat = "aac" + SpeechResponseFormatFlac SpeechResponseFormat = "flac" + SpeechResponseFormatWav SpeechResponseFormat = "wav" + SpeechResponseFormatPcm SpeechResponseFormat = "pcm" +) + +type CreateSpeechRequest struct { + Model SpeechModel `json:"model"` + Input string `json:"input"` + Voice SpeechVoice `json:"voice"` + ResponseFormat SpeechResponseFormat `json:"response_format,omitempty"` // Optional, default to mp3 + Speed float64 `json:"speed,omitempty"` // Optional, default to 1.0 +} + +func (c *Client) CreateSpeech(ctx context.Context, request CreateSpeechRequest) (response RawResponse, err error) { + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL("/audio/speech", withModel(string(request.Model))), + withBody(request), + withContentType("application/json"), + ) + if err != nil { + return + } + + return c.sendRequestRaw(req) +} diff --git a/vendor/github.com/sashabaranov/go-openai/stream.go b/vendor/github.com/sashabaranov/go-openai/stream.go new file mode 100644 index 00000000..a61c7c97 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/stream.go @@ -0,0 +1,55 @@ +package openai + +import ( + "context" + "errors" + "net/http" +) + +var ( + ErrTooManyEmptyStreamMessages = errors.New("stream has sent too many empty messages") +) + +type CompletionStream struct { + *streamReader[CompletionResponse] +} + +// CreateCompletionStream — API call to create a completion w/ streaming +// support. It sets whether to stream back partial progress. If set, tokens will be +// sent as data-only server-sent events as they become available, with the +// stream terminated by a data: [DONE] message. +func (c *Client) CreateCompletionStream( + ctx context.Context, + request CompletionRequest, +) (stream *CompletionStream, err error) { + urlSuffix := "/completions" + if !checkEndpointSupportsModel(urlSuffix, request.Model) { + err = ErrCompletionUnsupportedModel + return + } + + if !checkPromptType(request.Prompt) { + err = ErrCompletionRequestPromptTypeNotSupported + return + } + + request.Stream = true + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix, withModel(request.Model)), + withBody(request), + ) + if err != nil { + return nil, err + } + + resp, err := sendRequestStream[CompletionResponse](c, req) + if err != nil { + return + } + stream = &CompletionStream{ + streamReader: resp, + } + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/stream_reader.go b/vendor/github.com/sashabaranov/go-openai/stream_reader.go new file mode 100644 index 00000000..4210a194 --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/stream_reader.go @@ -0,0 +1,113 @@ +package openai + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net/http" + + utils "github.com/sashabaranov/go-openai/internal" +) + +var ( + headerData = []byte("data: ") + errorPrefix = []byte(`data: {"error":`) +) + +type streamable interface { + ChatCompletionStreamResponse | CompletionResponse +} + +type streamReader[T streamable] struct { + emptyMessagesLimit uint + isFinished bool + + reader *bufio.Reader + response *http.Response + errAccumulator utils.ErrorAccumulator + unmarshaler utils.Unmarshaler + + httpHeader +} + +func (stream *streamReader[T]) Recv() (response T, err error) { + if stream.isFinished { + err = io.EOF + return + } + + response, err = stream.processLines() + return +} + +//nolint:gocognit +func (stream *streamReader[T]) processLines() (T, error) { + var ( + emptyMessagesCount uint + hasErrorPrefix bool + ) + + for { + rawLine, readErr := stream.reader.ReadBytes('\n') + if readErr != nil || hasErrorPrefix { + respErr := stream.unmarshalError() + if respErr != nil { + return *new(T), fmt.Errorf("error, %w", respErr.Error) + } + return *new(T), readErr + } + + noSpaceLine := bytes.TrimSpace(rawLine) + if bytes.HasPrefix(noSpaceLine, errorPrefix) { + hasErrorPrefix = true + } + if !bytes.HasPrefix(noSpaceLine, headerData) || hasErrorPrefix { + if hasErrorPrefix { + noSpaceLine = bytes.TrimPrefix(noSpaceLine, headerData) + } + writeErr := stream.errAccumulator.Write(noSpaceLine) + if writeErr != nil { + return *new(T), writeErr + } + emptyMessagesCount++ + if emptyMessagesCount > stream.emptyMessagesLimit { + return *new(T), ErrTooManyEmptyStreamMessages + } + + continue + } + + noPrefixLine := bytes.TrimPrefix(noSpaceLine, headerData) + if string(noPrefixLine) == "[DONE]" { + stream.isFinished = true + return *new(T), io.EOF + } + + var response T + unmarshalErr := stream.unmarshaler.Unmarshal(noPrefixLine, &response) + if unmarshalErr != nil { + return *new(T), unmarshalErr + } + + return response, nil + } +} + +func (stream *streamReader[T]) unmarshalError() (errResp *ErrorResponse) { + errBytes := stream.errAccumulator.Bytes() + if len(errBytes) == 0 { + return + } + + err := stream.unmarshaler.Unmarshal(errBytes, &errResp) + if err != nil { + errResp = nil + } + + return +} + +func (stream *streamReader[T]) Close() error { + return stream.response.Body.Close() +} diff --git a/vendor/github.com/sashabaranov/go-openai/thread.go b/vendor/github.com/sashabaranov/go-openai/thread.go new file mode 100644 index 00000000..bc08e2bc --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/thread.go @@ -0,0 +1,171 @@ +package openai + +import ( + "context" + "net/http" +) + +const ( + threadsSuffix = "/threads" +) + +type Thread struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + Metadata map[string]any `json:"metadata"` + ToolResources ToolResources `json:"tool_resources,omitempty"` + + httpHeader +} + +type ThreadRequest struct { + Messages []ThreadMessage `json:"messages,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` + ToolResources *ToolResourcesRequest `json:"tool_resources,omitempty"` +} + +type ToolResources struct { + CodeInterpreter *CodeInterpreterToolResources `json:"code_interpreter,omitempty"` + FileSearch *FileSearchToolResources `json:"file_search,omitempty"` +} + +type CodeInterpreterToolResources struct { + FileIDs []string `json:"file_ids,omitempty"` +} + +type FileSearchToolResources struct { + VectorStoreIDs []string `json:"vector_store_ids,omitempty"` +} + +type ToolResourcesRequest struct { + CodeInterpreter *CodeInterpreterToolResourcesRequest `json:"code_interpreter,omitempty"` + FileSearch *FileSearchToolResourcesRequest `json:"file_search,omitempty"` +} + +type CodeInterpreterToolResourcesRequest struct { + FileIDs []string `json:"file_ids,omitempty"` +} + +type FileSearchToolResourcesRequest struct { + VectorStoreIDs []string `json:"vector_store_ids,omitempty"` + VectorStores []VectorStoreToolResources `json:"vector_stores,omitempty"` +} + +type VectorStoreToolResources struct { + FileIDs []string `json:"file_ids,omitempty"` + ChunkingStrategy *ChunkingStrategy `json:"chunking_strategy,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +type ChunkingStrategy struct { + Type ChunkingStrategyType `json:"type"` + Static *StaticChunkingStrategy `json:"static,omitempty"` +} + +type StaticChunkingStrategy struct { + MaxChunkSizeTokens int `json:"max_chunk_size_tokens"` + ChunkOverlapTokens int `json:"chunk_overlap_tokens"` +} + +type ChunkingStrategyType string + +const ( + ChunkingStrategyTypeAuto ChunkingStrategyType = "auto" + ChunkingStrategyTypeStatic ChunkingStrategyType = "static" +) + +type ModifyThreadRequest struct { + Metadata map[string]any `json:"metadata"` + ToolResources *ToolResources `json:"tool_resources,omitempty"` +} + +type ThreadMessageRole string + +const ( + ThreadMessageRoleAssistant ThreadMessageRole = "assistant" + ThreadMessageRoleUser ThreadMessageRole = "user" +) + +type ThreadMessage struct { + Role ThreadMessageRole `json:"role"` + Content string `json:"content"` + FileIDs []string `json:"file_ids,omitempty"` + Attachments []ThreadAttachment `json:"attachments,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +type ThreadAttachment struct { + FileID string `json:"file_id"` + Tools []ThreadAttachmentTool `json:"tools"` +} + +type ThreadAttachmentTool struct { + Type string `json:"type"` +} + +type ThreadDeleteResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + + httpHeader +} + +// CreateThread creates a new thread. +func (c *Client) CreateThread(ctx context.Context, request ThreadRequest) (response Thread, err error) { + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(threadsSuffix), withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveThread retrieves a thread. +func (c *Client) RetrieveThread(ctx context.Context, threadID string) (response Thread, err error) { + urlSuffix := threadsSuffix + "/" + threadID + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ModifyThread modifies a thread. +func (c *Client) ModifyThread( + ctx context.Context, + threadID string, + request ModifyThreadRequest, +) (response Thread, err error) { + urlSuffix := threadsSuffix + "/" + threadID + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// DeleteThread deletes a thread. +func (c *Client) DeleteThread( + ctx context.Context, + threadID string, +) (response ThreadDeleteResponse, err error) { + urlSuffix := threadsSuffix + "/" + threadID + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/github.com/sashabaranov/go-openai/vector_store.go b/vendor/github.com/sashabaranov/go-openai/vector_store.go new file mode 100644 index 00000000..682bb1cf --- /dev/null +++ b/vendor/github.com/sashabaranov/go-openai/vector_store.go @@ -0,0 +1,348 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +const ( + vectorStoresSuffix = "/vector_stores" + vectorStoresFilesSuffix = "/files" + vectorStoresFileBatchesSuffix = "/file_batches" +) + +type VectorStoreFileCount struct { + InProgress int `json:"in_progress"` + Completed int `json:"completed"` + Failed int `json:"failed"` + Cancelled int `json:"cancelled"` + Total int `json:"total"` +} + +type VectorStore struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + Name string `json:"name"` + UsageBytes int `json:"usage_bytes"` + FileCounts VectorStoreFileCount `json:"file_counts"` + Status string `json:"status"` + ExpiresAfter *VectorStoreExpires `json:"expires_after"` + ExpiresAt *int `json:"expires_at"` + Metadata map[string]any `json:"metadata"` + + httpHeader +} + +type VectorStoreExpires struct { + Anchor string `json:"anchor"` + Days int `json:"days"` +} + +// VectorStoreRequest provides the vector store request parameters. +type VectorStoreRequest struct { + Name string `json:"name,omitempty"` + FileIDs []string `json:"file_ids,omitempty"` + ExpiresAfter *VectorStoreExpires `json:"expires_after,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// VectorStoresList is a list of vector store. +type VectorStoresList struct { + VectorStores []VectorStore `json:"data"` + LastID *string `json:"last_id"` + FirstID *string `json:"first_id"` + HasMore bool `json:"has_more"` + httpHeader +} + +type VectorStoreDeleteResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + + httpHeader +} + +type VectorStoreFile struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + VectorStoreID string `json:"vector_store_id"` + UsageBytes int `json:"usage_bytes"` + Status string `json:"status"` + + httpHeader +} + +type VectorStoreFileRequest struct { + FileID string `json:"file_id"` +} + +type VectorStoreFilesList struct { + VectorStoreFiles []VectorStoreFile `json:"data"` + FirstID *string `json:"first_id"` + LastID *string `json:"last_id"` + HasMore bool `json:"has_more"` + + httpHeader +} + +type VectorStoreFileBatch struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + VectorStoreID string `json:"vector_store_id"` + Status string `json:"status"` + FileCounts VectorStoreFileCount `json:"file_counts"` + + httpHeader +} + +type VectorStoreFileBatchRequest struct { + FileIDs []string `json:"file_ids"` +} + +// CreateVectorStore creates a new vector store. +func (c *Client) CreateVectorStore(ctx context.Context, request VectorStoreRequest) (response VectorStore, err error) { + req, _ := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(vectorStoresSuffix), + withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion), + ) + + err = c.sendRequest(req, &response) + return +} + +// RetrieveVectorStore retrieves an vector store. +func (c *Client) RetrieveVectorStore( + ctx context.Context, + vectorStoreID string, +) (response VectorStore, err error) { + urlSuffix := fmt.Sprintf("%s/%s", vectorStoresSuffix, vectorStoreID) + req, _ := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// ModifyVectorStore modifies a vector store. +func (c *Client) ModifyVectorStore( + ctx context.Context, + vectorStoreID string, + request VectorStoreRequest, +) (response VectorStore, err error) { + urlSuffix := fmt.Sprintf("%s/%s", vectorStoresSuffix, vectorStoreID) + req, _ := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// DeleteVectorStore deletes an vector store. +func (c *Client) DeleteVectorStore( + ctx context.Context, + vectorStoreID string, +) (response VectorStoreDeleteResponse, err error) { + urlSuffix := fmt.Sprintf("%s/%s", vectorStoresSuffix, vectorStoreID) + req, _ := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// ListVectorStores Lists the currently available vector store. +func (c *Client) ListVectorStores( + ctx context.Context, + pagination Pagination, +) (response VectorStoresList, err error) { + urlValues := url.Values{} + + if pagination.After != nil { + urlValues.Add("after", *pagination.After) + } + if pagination.Order != nil { + urlValues.Add("order", *pagination.Order) + } + if pagination.Limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *pagination.Limit)) + } + if pagination.Before != nil { + urlValues.Add("before", *pagination.Before) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("%s%s", vectorStoresSuffix, encodedValues) + req, _ := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// CreateVectorStoreFile creates a new vector store file. +func (c *Client) CreateVectorStoreFile( + ctx context.Context, + vectorStoreID string, + request VectorStoreFileRequest, +) (response VectorStoreFile, err error) { + urlSuffix := fmt.Sprintf("%s/%s%s", vectorStoresSuffix, vectorStoreID, vectorStoresFilesSuffix) + req, _ := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// RetrieveVectorStoreFile retrieves a vector store file. +func (c *Client) RetrieveVectorStoreFile( + ctx context.Context, + vectorStoreID string, + fileID string, +) (response VectorStoreFile, err error) { + urlSuffix := fmt.Sprintf("%s/%s%s/%s", vectorStoresSuffix, vectorStoreID, vectorStoresFilesSuffix, fileID) + req, _ := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// DeleteVectorStoreFile deletes an existing file. +func (c *Client) DeleteVectorStoreFile( + ctx context.Context, + vectorStoreID string, + fileID string, +) (err error) { + urlSuffix := fmt.Sprintf("%s/%s%s/%s", vectorStoresSuffix, vectorStoreID, vectorStoresFilesSuffix, fileID) + req, _ := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, nil) + return +} + +// ListVectorStoreFiles Lists the currently available files for a vector store. +func (c *Client) ListVectorStoreFiles( + ctx context.Context, + vectorStoreID string, + pagination Pagination, +) (response VectorStoreFilesList, err error) { + urlValues := url.Values{} + if pagination.After != nil { + urlValues.Add("after", *pagination.After) + } + if pagination.Limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *pagination.Limit)) + } + if pagination.Before != nil { + urlValues.Add("before", *pagination.Before) + } + if pagination.Order != nil { + urlValues.Add("order", *pagination.Order) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("%s/%s%s%s", vectorStoresSuffix, vectorStoreID, vectorStoresFilesSuffix, encodedValues) + req, _ := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// CreateVectorStoreFileBatch creates a new vector store file batch. +func (c *Client) CreateVectorStoreFileBatch( + ctx context.Context, + vectorStoreID string, + request VectorStoreFileBatchRequest, +) (response VectorStoreFileBatch, err error) { + urlSuffix := fmt.Sprintf("%s/%s%s", vectorStoresSuffix, vectorStoreID, vectorStoresFileBatchesSuffix) + req, _ := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// RetrieveVectorStoreFileBatch retrieves a vector store file batch. +func (c *Client) RetrieveVectorStoreFileBatch( + ctx context.Context, + vectorStoreID string, + batchID string, +) (response VectorStoreFileBatch, err error) { + urlSuffix := fmt.Sprintf("%s/%s%s/%s", vectorStoresSuffix, vectorStoreID, vectorStoresFileBatchesSuffix, batchID) + req, _ := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// CancelVectorStoreFileBatch cancel a new vector store file batch. +func (c *Client) CancelVectorStoreFileBatch( + ctx context.Context, + vectorStoreID string, + batchID string, +) (response VectorStoreFileBatch, err error) { + urlSuffix := fmt.Sprintf("%s/%s%s/%s%s", vectorStoresSuffix, + vectorStoreID, vectorStoresFileBatchesSuffix, batchID, "/cancel") + req, _ := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} + +// ListVectorStoreFiles Lists the currently available files for a vector store. +func (c *Client) ListVectorStoreFilesInBatch( + ctx context.Context, + vectorStoreID string, + batchID string, + pagination Pagination, +) (response VectorStoreFilesList, err error) { + urlValues := url.Values{} + if pagination.After != nil { + urlValues.Add("after", *pagination.After) + } + if pagination.Limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *pagination.Limit)) + } + if pagination.Before != nil { + urlValues.Add("before", *pagination.Before) + } + if pagination.Order != nil { + urlValues.Add("order", *pagination.Order) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("%s/%s%s/%s%s%s", vectorStoresSuffix, + vectorStoreID, vectorStoresFileBatchesSuffix, batchID, "/files", encodedValues) + req, _ := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix), + withBetaAssistantVersion(c.config.AssistantVersion)) + + err = c.sendRequest(req, &response) + return +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c134628e..bb646e4e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,10 +1,6 @@ -# github.com/99designs/gqlgen v0.17.27 -## explicit; go 1.18 # github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da ## explicit github.com/aead/chacha20/chacha -# github.com/agnivade/levenshtein v1.1.1 -## explicit; go 1.13 # github.com/antlr4-go/antlr/v4 v4.13.1 ## explicit; go 1.22 github.com/antlr4-go/antlr/v4 @@ -25,13 +21,9 @@ github.com/coder/websocket/internal/errd github.com/coder/websocket/internal/util github.com/coder/websocket/internal/wsjs github.com/coder/websocket/internal/xsync -# github.com/cpuguy83/go-md2man/v2 v2.0.4 -## explicit; go 1.11 # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew -# github.com/dmarkham/enumer v1.5.8 -## explicit; go 1.17 # github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 ## explicit github.com/dustinkirkland/golang-petname @@ -108,7 +100,7 @@ github.com/hashicorp/hcl/json/token # github.com/inngest/expr v0.0.0-20241106234328-863dff7deec0 ## explicit; go 1.23.2 github.com/inngest/expr -# github.com/inngest/inngest v1.1.1-beta.1.0.20241119190743-29bd75bd8025 +# github.com/inngest/inngest v1.2.1-0.20241120122427-5ae5a84711e0 ## explicit; go 1.23.2 github.com/inngest/inngest/pkg/connect/types github.com/inngest/inngest/pkg/connect/wsproto @@ -129,6 +121,7 @@ github.com/inngest/inngest/pkg/telemetry/exporters github.com/inngest/inngest/pkg/telemetry/metrics github.com/inngest/inngest/pkg/telemetry/trace github.com/inngest/inngest/pkg/util +github.com/inngest/inngest/pkg/util/aigateway github.com/inngest/inngest/proto/gen/connect/v1 github.com/inngest/inngest/proto/gen/run/v2 # github.com/karlseguin/ccache/v2 v2.0.8 @@ -169,8 +162,6 @@ github.com/ohler55/ojg/jp # github.com/oklog/ulid/v2 v2.1.0 ## explicit; go 1.15 github.com/oklog/ulid/v2 -# github.com/pascaldekloe/name v1.0.1 -## explicit; go 1.10 # github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 ## explicit; go 1.16 github.com/pbnjay/memory @@ -208,8 +199,10 @@ github.com/robfig/cron/v3 github.com/rs/zerolog github.com/rs/zerolog/internal/cbor github.com/rs/zerolog/internal/json -# github.com/russross/blackfriday/v2 v2.1.0 -## explicit +# github.com/sashabaranov/go-openai v1.35.6 +## explicit; go 1.18 +github.com/sashabaranov/go-openai +github.com/sashabaranov/go-openai/internal # github.com/sourcegraph/conc v0.3.0 ## explicit; go 1.19 github.com/sourcegraph/conc @@ -250,15 +243,9 @@ github.com/subosito/gotenv # github.com/tidwall/btree v1.7.0 ## explicit; go 1.19 github.com/tidwall/btree -# github.com/urfave/cli/v2 v2.25.1 -## explicit; go 1.18 -# github.com/vektah/gqlparser/v2 v2.5.15 -## explicit; go 1.19 # github.com/xhit/go-str2duration/v2 v2.1.0 ## explicit; go 1.13 github.com/xhit/go-str2duration/v2 -# github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 -## explicit # go.opentelemetry.io/otel v1.28.0 ## explicit; go 1.21 go.opentelemetry.io/otel @@ -370,8 +357,6 @@ golang.org/x/exp/slices golang.org/x/exp/slog golang.org/x/exp/slog/internal golang.org/x/exp/slog/internal/buffer -# golang.org/x/mod v0.21.0 -## explicit; go 1.22.0 # golang.org/x/net v0.30.0 ## explicit; go 1.18 golang.org/x/net/http/httpguts @@ -405,8 +390,6 @@ golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm -# golang.org/x/tools v0.26.0 -## explicit; go 1.22.0 # google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 ## explicit; go 1.21 google.golang.org/genproto/googleapis/api/expr/v1alpha1 From e44632909d58cb7292f5f468076b26c136792961 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Thu, 21 Nov 2024 16:43:32 +0100 Subject: [PATCH 02/16] wip --- connect.go | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/connect.go b/connect.go index b722ba68..29648ca8 100644 --- a/connect.go +++ b/connect.go @@ -18,6 +18,7 @@ import ( "golang.org/x/sync/errgroup" "google.golang.org/protobuf/proto" "io" + "net" "net/url" "runtime" "sync" @@ -107,7 +108,20 @@ func (h *handler) Connect(ctx context.Context) error { go ch.workerPool(ctx) } - return ch.Connect(ctx) + defer func() { + // TODO Push remaining messages to another destination for processing? + }() + + err := ch.Connect(ctx) + if err != nil { + if ctx.Err() != nil { + return nil + } + + return fmt.Errorf("could not establish connection: %w", err) + } + + return nil } func (h *connectHandler) instanceId() string { @@ -329,7 +343,7 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE func (h *connectHandler) sendBufferedMessages(ws *websocket.Conn) error { processed := 0 for _, msg := range h.messageBuffer { - // always send the message, even if the context is canceled + // always send the message, even if the context is cancelled err := wsproto.Write(context.Background(), ws, msg) if err != nil { // Only send buffered messages once @@ -406,8 +420,8 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa // If read loop ends, this can be for two reasons // - Connection loss (io.EOF), read loop terminated intentionally (CloseError), other error (unexpected) - // - Worker shutdown, parent context got canceled - if err := eg.Wait(); err != nil { + // - Worker shutdown, parent context got cancelled + if err := eg.Wait(); err != nil && ctx.Err() == nil { h.h.Logger.Debug("read loop ended with error", "err", err) // In case the gateway intentionally closed the connection, we'll receive a close error @@ -420,18 +434,16 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa } // connection closed without reason - if errors.Is(err, io.EOF) { + if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { h.h.Logger.Error("failed to read message from gateway, lost connection unexpectedly", "err", err) return true, fmt.Errorf("connection closed unexpectedly: %w", cerr) } // If this is not a worker shutdown, we should reconnect - if ctx.Err() == nil { - return true, fmt.Errorf("connection closed unexpectedly: %w", ctx.Err()) - } + return true, fmt.Errorf("connection closed unexpectedly: %w", ctx.Err()) } - // Perform graceful shutdown routine + // Perform graceful shutdown routine (context was cancelled) // Signal gateway that we won't process additional messages! { @@ -479,6 +491,8 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa continue } } + + // TODO Push remaining messages to another destination for processing? } // Attempt to shut down connection if not already done @@ -501,7 +515,7 @@ func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, h err = handler(ws) if err != nil { - return false, err + return true, err } return false, nil @@ -602,6 +616,7 @@ func (h *connectHandler) connectInvoke(ctx context.Context, ws *websocket.Conn, } // Ack message + // If we're shutting down (context is canceled) we will not ack, which is desired! if err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ Kind: connectproto.GatewayMessageType_WORKER_REQUEST_ACK, Payload: ackPayload, @@ -649,7 +664,8 @@ func (h *connectHandler) connectInvoke(ctx context.Context, ws *websocket.Conn, stepId = body.StepId } - resp, ops, err := invoke(ctx, fn, &request, stepId) + // Invoke function, always complete regardless of + resp, ops, err := invoke(context.Background(), fn, &request, stepId) // NOTE: When triggering step errors, we should have an OpcodeStepError // within ops alongside an error. We can safely ignore that error, as it's From 396befa2a89332edadc75daaee1f6bfb3c1032f2 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Thu, 21 Nov 2024 19:01:42 +0100 Subject: [PATCH 03/16] allow multiple connections, mostly complete draining logic --- connect.go | 408 ++++++++++++----- go.mod | 4 +- go.sum | 4 +- .../proto/gen/connect/v1/connect.pb.go | 418 ++++++++++-------- vendor/modules.txt | 24 +- 5 files changed, 536 insertions(+), 322 deletions(-) diff --git a/connect.go b/connect.go index 4f2de58a..47ab1b4c 100644 --- a/connect.go +++ b/connect.go @@ -46,6 +46,64 @@ type connectHandler struct { inProgress sync.WaitGroup workerPoolMsgs chan workerPoolMsg + + hostsManager *hostsManager +} + +type hostsManager struct { + gatewayHosts []string + availableGatewayHosts map[string]struct{} + drainingGatewayHosts map[string]struct{} + unreachableGatewayHosts map[string]struct{} + hostsLock sync.RWMutex +} + +func newHostsManager(gatewayHosts []string) *hostsManager { + hm := &hostsManager{ + gatewayHosts: gatewayHosts, + availableGatewayHosts: make(map[string]struct{}), + drainingGatewayHosts: make(map[string]struct{}), + unreachableGatewayHosts: make(map[string]struct{}), + } + + hm.resetGateways() + + return hm +} + +func (h *hostsManager) pickAvailableGateway() string { + h.hostsLock.RLock() + defer h.hostsLock.RUnlock() + + for host := range h.availableGatewayHosts { + return host + } + return "" +} + +func (h *hostsManager) markDrainingGateway(host string) { + h.hostsLock.Lock() + defer h.hostsLock.Unlock() + delete(h.availableGatewayHosts, host) + h.drainingGatewayHosts[host] = struct{}{} +} + +func (h *hostsManager) markUnreachableGateway(host string) { + h.hostsLock.Lock() + defer h.hostsLock.Unlock() + delete(h.availableGatewayHosts, host) + h.unreachableGatewayHosts[host] = struct{}{} +} + +func (h *hostsManager) resetGateways() { + h.hostsLock.Lock() + defer h.hostsLock.Unlock() + h.availableGatewayHosts = make(map[string]struct{}) + h.drainingGatewayHosts = make(map[string]struct{}) + h.unreachableGatewayHosts = make(map[string]struct{}) + for _, host := range h.gatewayHosts { + h.availableGatewayHosts[host] = struct{}{} + } } // authContext is wrapper for information related to authentication @@ -66,32 +124,6 @@ func (h *connectHandler) connectURLs() []string { return nil } -func (h *connectHandler) connectToGateway(ctx context.Context) (*websocket.Conn, error) { - hosts := h.connectURLs() - if len(hosts) == 0 { - return nil, fmt.Errorf("no connect URLs provided") - } - - for _, gatewayHost := range hosts { - h.h.Logger.Debug("attempting to connect", "host", gatewayHost) - - // Establish WebSocket connection to one of the gateways - ws, _, err := websocket.Dial(ctx, gatewayHost, &websocket.DialOptions{ - Subprotocols: []string{ - types.GatewaySubProtocol, - }, - }) - if err != nil { - // try next connection - continue - } - - return ws, nil - } - - return nil, fmt.Errorf("could not establish outbound connection: no available gateway host") -} - func (h *handler) Connect(ctx context.Context) error { h.useConnect = true concurrency := h.HandlerOpts.GetWorkerConcurrency() @@ -179,54 +211,121 @@ func (h *connectHandler) Connect(ctx context.Context) error { return fmt.Errorf("failed to serialize connect config: %w", err) } - var attempts int - for { - attempts++ + hosts := h.connectURLs() + if len(hosts) == 0 { + return fmt.Errorf("no connect URLs provided") + } - if attempts == 5 { - return fmt.Errorf("could not establish connection after 5 attempts") - } + h.hostsManager = newHostsManager(hosts) - shouldReconnect, err := h.connect(ctx, connectionEstablishData{ - signingKey: auth.signingKey, - numCpuCores: int32(numCpuCores), - totalMem: int64(totalMem), - marshaledFns: marshaledFns, - marshaledCapabilities: marshaledCapabilities, - }) + eg := errgroup.Group{} - h.h.Logger.Error("connect failed", "err", err, "reconnect", shouldReconnect) + notifyConnectDoneChan := make(chan connectReport) + notifyConnectedChan := make(chan struct{}) + initiateConnectionChan := make(chan struct{}) - if !shouldReconnect { - return err - } + var attempts int + eg.Go(func() error { + for { + select { + case <-ctx.Done(): + return nil + case <-notifyConnectedChan: + attempts = 0 + continue + case msg := <-notifyConnectDoneChan: + h.h.Logger.Error("connect failed", "err", err, "reconnect", msg.reconnect) - closeErr := websocket.CloseError{} - if errors.As(err, &closeErr) { - switch closeErr.Reason { - // If auth failed, retry with fallback key - case syscode.CodeConnectAuthFailed: - if auth.fallback { - return fmt.Errorf("failed to authenticate with fallback key, exiting") + if !msg.reconnect { + return err } - signingKeyFallback := h.h.GetSigningKeyFallback() - if signingKeyFallback != "" { - auth = authContext{signingKey: signingKeyFallback, fallback: true} + if msg.err != nil { + closeErr := websocket.CloseError{} + if errors.As(err, &closeErr) { + switch closeErr.Reason { + // If auth failed, retry with fallback key + case syscode.CodeConnectAuthFailed: + if auth.fallback { + return fmt.Errorf("failed to authenticate with fallback key, exiting") + } + + signingKeyFallback := h.h.GetSigningKeyFallback() + if signingKeyFallback != "" { + auth = authContext{signingKey: signingKeyFallback, fallback: true} + } + + initiateConnectionChan <- struct{}{} + + continue + + // Retry on the following error codes + case syscode.CodeConnectGatewayClosing, syscode.CodeConnectInternal, syscode.CodeConnectWorkerHelloTimeout: + initiateConnectionChan <- struct{}{} + + continue + + default: + // If we received a reason that's non-retriable, stop here. + return fmt.Errorf("connect failed with error code %q", closeErr.Reason) + } + } } - continue + initiateConnectionChan <- struct{}{} + case <-initiateConnectionChan: + } - // Retry on the following error codes - case syscode.CodeConnectGatewayClosing, syscode.CodeConnectInternal, syscode.CodeConnectWorkerHelloTimeout: - continue + if attempts == 5 { + return fmt.Errorf("could not establish connection after 5 attempts") + } - default: - // If we received a reason that's non-retriable, stop here. - return fmt.Errorf("connect failed with error code %q", closeErr.Reason) + attempts++ + + h.connect(ctx, false, connectionEstablishData{ + signingKey: auth.signingKey, + numCpuCores: int32(numCpuCores), + totalMem: int64(totalMem), + marshaledFns: marshaledFns, + marshaledCapabilities: marshaledCapabilities, + }, notifyConnectedChan, notifyConnectDoneChan) + } + }) + + initiateConnectionChan <- struct{}{} + + if err := eg.Wait(); err != nil { + return fmt.Errorf("could not establish connection: %w", err) + } + + // Send out buffered messages, using new connection if necessary! + h.messageBufferLock.Lock() + defer h.messageBufferLock.Unlock() + if len(h.messageBuffer) > 0 { + // Send buffered messages via a working connection + err = h.withTemporaryConnection(connectionEstablishData{ + signingKey: auth.signingKey, + numCpuCores: int32(numCpuCores), + totalMem: int64(totalMem), + marshaledFns: marshaledFns, + marshaledCapabilities: marshaledCapabilities, + }, func(ws *websocket.Conn) error { + // Send buffered messages + err := h.sendBufferedMessages(ws) + if err != nil { + return fmt.Errorf("could not send buffered messages: %w", err) } + + return nil + }) + if err != nil { + h.h.Logger.Error("could not establish connection for sending buffered messages", "err", err) } + + // TODO Push remaining messages to another destination for processing? } + + return nil } type connectionEstablishData struct { @@ -238,13 +337,31 @@ type connectionEstablishData struct { manualReadinessAck bool } -func (h *connectHandler) prepareConnection(ctx context.Context, data connectionEstablishData) (*websocket.Conn, bool, error) { +type preparedConnection struct { + ws *websocket.Conn + gatewayHost string +} + +func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGateways bool, data connectionEstablishData) (preparedConnection, bool, error) { connectTimeout, cancelConnectTimeout := context.WithTimeout(ctx, 10*time.Second) defer cancelConnectTimeout() - ws, err := h.connectToGateway(connectTimeout) + gatewayHost := h.hostsManager.pickAvailableGateway() + if gatewayHost == "" { + h.hostsManager.resetGateways() + + return preparedConnection{}, allowResettingGateways, fmt.Errorf("no available gateway hosts") + } + + // Establish WebSocket connection to one of the gateways + ws, _, err := websocket.Dial(connectTimeout, gatewayHost, &websocket.DialOptions{ + Subprotocols: []string{ + types.GatewaySubProtocol, + }, + }) if err != nil { - return nil, false, fmt.Errorf("could not connect: %w", err) + h.hostsManager.markUnreachableGateway(gatewayHost) + return preparedConnection{}, false, fmt.Errorf("could not connect to gateway: %w", err) } // Connection ID is unique per connection, reconnections should get a new ID @@ -259,11 +376,13 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE var helloMessage connectproto.ConnectMessage err = wsproto.Read(initialMessageTimeout, ws, &helloMessage) if err != nil { - return nil, true, fmt.Errorf("did not receive gateway hello message: %w", err) + h.hostsManager.markUnreachableGateway(gatewayHost) + return preparedConnection{}, true, fmt.Errorf("did not receive gateway hello message: %w", err) } if helloMessage.Kind != connectproto.GatewayMessageType_GATEWAY_HELLO { - return nil, true, fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind) + h.hostsManager.markUnreachableGateway(gatewayHost) + return preparedConnection{}, true, fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind) } h.h.Logger.Debug("received gateway hello message") @@ -273,7 +392,7 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE { hashedKey, err := hashedSigningKey([]byte(data.signingKey)) if err != nil { - return nil, false, fmt.Errorf("could not hash signing key: %w", err) + return preparedConnection{}, false, fmt.Errorf("could not hash signing key: %w", err) } apiOrigin := h.h.GetAPIBaseURL() @@ -308,7 +427,7 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE WorkerManualReadinessAck: data.manualReadinessAck, }) if err != nil { - return nil, false, fmt.Errorf("could not serialize sdk connect message: %w", err) + return preparedConnection{}, false, fmt.Errorf("could not serialize sdk connect message: %w", err) } err = wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ @@ -316,7 +435,7 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE Payload: data, }) if err != nil { - return nil, true, fmt.Errorf("could not send initial message") + return preparedConnection{}, true, fmt.Errorf("could not send initial message") } } @@ -327,17 +446,17 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE var connectionReadyMsg connectproto.ConnectMessage err = wsproto.Read(connectionReadyTimeout, ws, &connectionReadyMsg) if err != nil { - return nil, true, fmt.Errorf("did not receive gateway connection ready message: %w", err) + return preparedConnection{}, true, fmt.Errorf("did not receive gateway connection ready message: %w", err) } if connectionReadyMsg.Kind != connectproto.GatewayMessageType_GATEWAY_CONNECTION_READY { - return nil, true, fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind) + return preparedConnection{}, true, fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind) } h.h.Logger.Debug("received gateway connection ready message") } - return ws, false, nil + return preparedConnection{ws, gatewayHost}, false, nil } func (h *connectHandler) sendBufferedMessages(ws *websocket.Conn) error { @@ -360,20 +479,22 @@ func (h *connectHandler) sendBufferedMessages(ws *websocket.Conn) error { return nil } -func (h *connectHandler) connect(ctx context.Context, data connectionEstablishData) (reconnect bool, err error) { +var errGatewayDraining = errors.New("gateway is draining") + +func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) (reconnect bool, err error) { ctx, cancel := context.WithCancel(ctx) defer cancel() - ws, reconnect, err := h.prepareConnection(ctx, data) - if err != nil { - return reconnect, fmt.Errorf("could not establish connection: %w", err) - } - defer func() { // TODO Do we need to include a reason here? If we only use this for unexpected disconnects, probably not _ = ws.CloseNow() }() + go func() { + <-ctx.Done() + _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) + }() + // Send buffered but unsent messages if connection was re-established if len(h.messageBuffer) > 0 { h.h.Logger.Debug("sending buffered messages", "count", len(h.messageBuffer)) @@ -386,12 +507,8 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa eg := errgroup.Group{} eg.Go(func() error { for { - if ctx.Err() != nil { - return ctx.Err() - } - var msg connectproto.ConnectMessage - err = wsproto.Read(ctx, ws, &msg) + err = wsproto.Read(context.Background(), ws, &msg) if err != nil { h.h.Logger.Error("failed to read message", "err", err) @@ -402,6 +519,10 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa h.h.Logger.Debug("received gateway request", "msg", &msg) switch msg.Kind { + case connectproto.GatewayMessageType_GATEWAY_CLOSING: + // Stop the read loop: We will not receive any further messages and should establish a new connection + // We can still use the old connection to send replies to the gateway + return errGatewayDraining case connectproto.GatewayMessageType_GATEWAY_EXECUTOR_REQUEST: // Handle invoke in a non-blocking way to allow for other messages to be processed h.inProgress.Add(1) @@ -422,6 +543,32 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa // - Connection loss (io.EOF), read loop terminated intentionally (CloseError), other error (unexpected) // - Worker shutdown, parent context got cancelled if err := eg.Wait(); err != nil && ctx.Err() == nil { + if errors.Is(err, errGatewayDraining) { + h.hostsManager.markDrainingGateway(gatewayHost) + + // Gateway is draining and will not accept new connections. + // We must reconnect to a different gateway, only then can we close the old connection. + waitUntilConnected, doneWaiting := context.WithCancel(context.Background()) + defer doneWaiting() + + // Intercept connected signal and pass it to the main goroutine + notifyConnectedInterceptChan := make(chan struct{}) + go func() { + <-notifyConnectedChan + notifyConnectedInterceptChan <- struct{}{} + doneWaiting() + }() + + // Establish new connection and pass close reports back to the main goroutine + h.connect(ctx, true, data, notifyConnectedInterceptChan, notifyConnectDoneChan) + + // Wait until the new connection is established before closing the old one + <-waitUntilConnected.Done() + + // By returning, we will close the old connection + return false, errGatewayDraining + } + h.h.Logger.Debug("read loop ended with error", "err", err) // In case the gateway intentionally closed the connection, we'll receive a close error @@ -462,63 +609,82 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa // Wait until all in-progress requests are completed h.inProgress.Wait() - // Send out buffered messages, using new connection if necessary! - h.messageBufferLock.Lock() - defer h.messageBufferLock.Unlock() - if len(h.messageBuffer) > 0 { - attempts := 0 - for { - attempts++ - if attempts == 3 { - h.h.Logger.Error("could not establish connection after 3 attempts") - break - } + // Attempt to shut down connection if not already done + _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) - reconnect, err = h.withTemporaryConnection(data, func(ws *websocket.Conn) error { - // Send buffered messages - err := h.sendBufferedMessages(ws) - if err != nil { - return fmt.Errorf("could not send buffered messages: %w", err) - } + return false, nil +} - return nil - }) - if err != nil { - if !reconnect { - h.h.Logger.Error("could not establish connection for sending buffered messages", "err", err) - break - } - continue - } - } +type connectReport struct { + reconnect bool + err error +} - // TODO Push remaining messages to another destination for processing? +func (h *connectHandler) connect(ctx context.Context, allowResettingGateways bool, data connectionEstablishData, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) { + preparedConn, reconnect, err := h.prepareConnection(ctx, allowResettingGateways, data) + if err != nil { + notifyConnectDoneChan <- connectReport{ + reconnect: reconnect, + err: fmt.Errorf("could not establish connection: %w", err), + } + return } - // Attempt to shut down connection if not already done - _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) + notifyConnectedChan <- struct{}{} - return false, nil + reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost, notifyConnectedChan, notifyConnectDoneChan) + if err != nil { + if errors.Is(err, errGatewayDraining) { + // if the gateway is draining, the original connection was closed, and we already reconnected inside handleConnection + return + } + + notifyConnectDoneChan <- connectReport{ + reconnect: reconnect, + err: fmt.Errorf("could not handle connection: %w", err), + } + return + } + + notifyConnectDoneChan <- connectReport{ + reconnect: reconnect, + err: nil, + } } -func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, handler func(ws *websocket.Conn) error) (bool, error) { +func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, handler func(ws *websocket.Conn) error) error { // Prevent this connection from receiving work data.manualReadinessAck = true - ws, reconnect, err := h.prepareConnection(context.Background(), data) - if err != nil { - return reconnect, fmt.Errorf("could not establish temporary connection: %w", err) + maxAttempts := 4 + + var conn *websocket.Conn + var attempts int + for { + if attempts == maxAttempts { + return fmt.Errorf("could not establish connection after %d attempts", maxAttempts) + } + + ws, _, err := h.prepareConnection(context.Background(), true, data) + if err != nil { + attempts++ + continue + } + + conn = ws.ws + break } + defer func() { - _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) + _ = conn.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) }() - err = handler(ws) + err := handler(conn) if err != nil { - return true, err + return err } - return false, nil + return nil } func (h *connectHandler) processExecutorRequest(ws *websocket.Conn, msg *connectproto.ConnectMessage) { diff --git a/go.mod b/go.mod index 554d672e..304f5732 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,10 @@ require ( github.com/google/uuid v1.6.0 github.com/gosimple/slug v1.12.0 github.com/gowebpki/jcs v1.0.0 - github.com/inngest/inngest v1.2.1-0.20241120122427-5ae5a84711e0 + github.com/inngest/inngest v1.2.1-0.20241121162138-fa921e44acf1 github.com/oklog/ulid/v2 v2.1.0 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 + github.com/sashabaranov/go-openai v1.35.6 github.com/stretchr/testify v1.9.0 github.com/xhit/go-str2duration/v2 v2.1.0 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c @@ -54,7 +55,6 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/zerolog v1.26.1 // indirect - github.com/sashabaranov/go-openai v1.35.6 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.4.1 // indirect diff --git a/go.sum b/go.sum index fbe15eb9..e1fb9e0b 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inngest/expr v0.0.0-20241106234328-863dff7deec0 h1:cqaGD0mx745BYyVGZ3GJsiZETktC64h1r9zdDInmKIs= github.com/inngest/expr v0.0.0-20241106234328-863dff7deec0/go.mod h1:0Dllw9clwlMWWxfiSsHY9VdE+Zjt/8SVCMxK9r39aKE= -github.com/inngest/inngest v1.2.1-0.20241120122427-5ae5a84711e0 h1:/J6q4xMwYvPRGy2Pz4e5sCrw79eq6yIGq4EDwujdON0= -github.com/inngest/inngest v1.2.1-0.20241120122427-5ae5a84711e0/go.mod h1:+Nl49tpdy+gRzEYHgEb3eNV7aheh8iXyP15XKc+nVU4= +github.com/inngest/inngest v1.2.1-0.20241121162138-fa921e44acf1 h1:yVbhmibleJZrXqT4IUIqaA+tAAf4iCmVi2eIct8QHwI= +github.com/inngest/inngest v1.2.1-0.20241121162138-fa921e44acf1/go.mod h1:+Nl49tpdy+gRzEYHgEb3eNV7aheh8iXyP15XKc+nVU4= github.com/karlseguin/ccache/v2 v2.0.8 h1:lT38cE//uyf6KcFok0rlgXtGFBWxkI6h/qg4tbFyDnA= github.com/karlseguin/ccache/v2 v2.0.8/go.mod h1:2BDThcfQMf/c0jnZowt16eW405XIqZPavt+HoYEtcxQ= github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003 h1:vJ0Snvo+SLMY72r5J4sEfkuE7AFbixEP2qRbEcum/wA= diff --git a/vendor/github.com/inngest/inngest/proto/gen/connect/v1/connect.pb.go b/vendor/github.com/inngest/inngest/proto/gen/connect/v1/connect.pb.go index 8f7a5651..226ec61e 100644 --- a/vendor/github.com/inngest/inngest/proto/gen/connect/v1/connect.pb.go +++ b/vendor/github.com/inngest/inngest/proto/gen/connect/v1/connect.pb.go @@ -9,6 +9,7 @@ package connect import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -31,6 +32,8 @@ const ( GatewayMessageType_WORKER_REQUEST_ACK GatewayMessageType = 5 GatewayMessageType_WORKER_REPLY GatewayMessageType = 6 GatewayMessageType_WORKER_PAUSE GatewayMessageType = 7 + GatewayMessageType_WORKER_HEARTBEAT GatewayMessageType = 8 + GatewayMessageType_GATEWAY_CLOSING GatewayMessageType = 9 ) // Enum value maps for GatewayMessageType. @@ -44,6 +47,8 @@ var ( 5: "WORKER_REQUEST_ACK", 6: "WORKER_REPLY", 7: "WORKER_PAUSE", + 8: "WORKER_HEARTBEAT", + 9: "GATEWAY_CLOSING", } GatewayMessageType_value = map[string]int32{ "GATEWAY_HELLO": 0, @@ -54,6 +59,8 @@ var ( "WORKER_REQUEST_ACK": 5, "WORKER_REPLY": 6, "WORKER_PAUSE": 7, + "WORKER_HEARTBEAT": 8, + "GATEWAY_CLOSING": 9, } ) @@ -932,14 +939,15 @@ type ConnMetadata struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - GatewayId string `protobuf:"bytes,2,opt,name=gateway_id,json=gatewayId,proto3" json:"gateway_id,omitempty"` - InstanceId string `protobuf:"bytes,3,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` - GroupId string `protobuf:"bytes,4,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` - Status ConnectionStatus `protobuf:"varint,5,opt,name=status,proto3,enum=connect.v1.ConnectionStatus" json:"status,omitempty"` - Language string `protobuf:"bytes,6,opt,name=language,proto3" json:"language,omitempty"` - Version string `protobuf:"bytes,7,opt,name=version,proto3" json:"version,omitempty"` - Attributes *SystemAttributes `protobuf:"bytes,8,opt,name=attributes,proto3" json:"attributes,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + GatewayId string `protobuf:"bytes,2,opt,name=gateway_id,json=gatewayId,proto3" json:"gateway_id,omitempty"` + InstanceId string `protobuf:"bytes,3,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + GroupId string `protobuf:"bytes,4,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + Status ConnectionStatus `protobuf:"varint,5,opt,name=status,proto3,enum=connect.v1.ConnectionStatus" json:"status,omitempty"` + LastHeartbeatAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=last_heartbeat_at,json=lastHeartbeatAt,proto3" json:"last_heartbeat_at,omitempty"` + Language string `protobuf:"bytes,7,opt,name=language,proto3" json:"language,omitempty"` + Version string `protobuf:"bytes,8,opt,name=version,proto3" json:"version,omitempty"` + Attributes *SystemAttributes `protobuf:"bytes,9,opt,name=attributes,proto3" json:"attributes,omitempty"` } func (x *ConnMetadata) Reset() { @@ -1007,6 +1015,13 @@ func (x *ConnMetadata) GetStatus() ConnectionStatus { return ConnectionStatus_CONNECTED } +func (x *ConnMetadata) GetLastHeartbeatAt() *timestamppb.Timestamp { + if x != nil { + return x.LastHeartbeatAt + } + return nil +} + func (x *ConnMetadata) GetLanguage() string { if x != nil { return x.Language @@ -1179,187 +1194,196 @@ var File_connect_v1_connect_proto protoreflect.FileDescriptor var file_connect_v1_connect_proto_rawDesc = []byte{ 0x0a, 0x18, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x22, 0x5e, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x32, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, - 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, - 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x86, 0x01, 0x0a, 0x11, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, - 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x23, 0x0a, - 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x88, - 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x22, - 0x73, 0x0a, 0x0e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, - 0x73, 0x12, 0x3c, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, - 0x23, 0x0a, 0x0d, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x48, 0x61, 0x73, 0x68, 0x22, 0x70, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x65, - 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, - 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x63, 0x61, 0x70, - 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x75, 0x6e, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x66, 0x75, - 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x70, 0x69, 0x5f, 0x6f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x70, 0x69, - 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0x6e, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x44, 0x61, - 0x74, 0x61, 0x12, 0x2c, 0x0a, 0x12, 0x68, 0x61, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x73, 0x69, 0x67, - 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, - 0x68, 0x61, 0x73, 0x68, 0x65, 0x64, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, - 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, - 0x15, 0x0a, 0x06, 0x65, 0x6e, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x65, 0x6e, 0x76, 0x49, 0x64, 0x22, 0xaa, 0x04, 0x0a, 0x18, 0x57, 0x6f, 0x72, 0x6b, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, - 0x61, 0x74, 0x61, 0x12, 0x3c, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, - 0x64, 0x12, 0x31, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, - 0x44, 0x61, 0x74, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x31, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x3d, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, - 0x75, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x65, 0x73, 0x73, 0x5f, 0x61, 0x63, - 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4d, - 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x65, 0x73, 0x73, 0x41, 0x63, - 0x6b, 0x12, 0x49, 0x0a, 0x11, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x10, 0x73, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0b, - 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, - 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, - 0x6b, 0x12, 0x1f, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x88, - 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x64, 0x6b, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x64, 0x6b, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x64, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x67, 0x75, - 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x64, 0x6b, 0x4c, 0x61, - 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x65, 0x6e, 0x76, 0x69, 0x72, - 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x70, 0x6c, 0x61, 0x74, 0x66, - 0x6f, 0x72, 0x6d, 0x22, 0x48, 0x0a, 0x16, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x53, 0x79, - 0x6e, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x20, 0x0a, - 0x09, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x00, 0x52, 0x08, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, - 0x0c, 0x0a, 0x0a, 0x5f, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x5f, 0x69, 0x64, 0x22, 0xe1, 0x01, - 0x0a, 0x1a, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x65, - 0x6e, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6e, 0x76, - 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x61, 0x70, 0x70, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x75, 0x6e, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6c, 0x75, 0x67, 0x12, 0x1c, - 0x0a, 0x07, 0x73, 0x74, 0x65, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, - 0x00, 0x52, 0x06, 0x73, 0x74, 0x65, 0x70, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x27, 0x0a, 0x0f, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x50, 0x61, - 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x74, 0x65, 0x70, 0x5f, 0x69, - 0x64, 0x22, 0x9b, 0x01, 0x0a, 0x14, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x41, 0x63, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x70, 0x70, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x70, 0x70, 0x49, 0x64, - 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x6c, 0x75, - 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x53, 0x6c, 0x75, 0x67, 0x12, 0x1c, 0x0a, 0x07, 0x73, 0x74, 0x65, 0x70, 0x5f, 0x69, 0x64, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x65, 0x70, 0x49, 0x64, - 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x74, 0x65, 0x70, 0x5f, 0x69, 0x64, 0x22, - 0xe9, 0x01, 0x0a, 0x0b, 0x53, 0x44, 0x4b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x35, - 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, - 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x44, 0x4b, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x5f, - 0x72, 0x65, 0x74, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, 0x52, - 0x65, 0x74, 0x72, 0x79, 0x12, 0x24, 0x0a, 0x0b, 0x72, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x61, 0x66, - 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x72, 0x65, 0x74, - 0x72, 0x79, 0x41, 0x66, 0x74, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x64, - 0x6b, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x73, 0x64, 0x6b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, - 0x72, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x22, 0xa3, 0x02, 0x0a, 0x0c, - 0x43, 0x6f, 0x6e, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, - 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x69, - 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x0a, - 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x22, 0x5c, 0x0a, 0x10, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x70, 0x75, 0x5f, 0x63, 0x6f, 0x72, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, - 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x65, 0x6d, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x65, 0x6d, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, - 0x0e, 0x0a, 0x02, 0x6f, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x73, 0x22, - 0xd4, 0x01, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x15, 0x0a, - 0x06, 0x65, 0x6e, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, - 0x6e, 0x76, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x70, 0x70, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x68, - 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, - 0x2e, 0x0a, 0x05, 0x63, 0x6f, 0x6e, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x05, 0x63, 0x6f, 0x6e, 0x6e, 0x73, 0x12, - 0x1c, 0x0a, 0x07, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x00, 0x52, 0x06, 0x73, 0x79, 0x6e, 0x63, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x1e, 0x0a, - 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, - 0x01, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, - 0x08, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x69, 0x64, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x2a, 0xc5, 0x01, 0x0a, 0x12, 0x47, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, - 0x0d, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x5f, 0x48, 0x45, 0x4c, 0x4c, 0x4f, 0x10, 0x00, - 0x12, 0x12, 0x0a, 0x0e, 0x57, 0x4f, 0x52, 0x4b, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, - 0x43, 0x54, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x5f, - 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, - 0x10, 0x02, 0x12, 0x1c, 0x0a, 0x18, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x5f, 0x45, 0x58, - 0x45, 0x43, 0x55, 0x54, 0x4f, 0x52, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x03, - 0x12, 0x10, 0x0a, 0x0c, 0x57, 0x4f, 0x52, 0x4b, 0x45, 0x52, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, - 0x10, 0x04, 0x12, 0x16, 0x0a, 0x12, 0x57, 0x4f, 0x52, 0x4b, 0x45, 0x52, 0x5f, 0x52, 0x45, 0x51, - 0x55, 0x45, 0x53, 0x54, 0x5f, 0x41, 0x43, 0x4b, 0x10, 0x05, 0x12, 0x10, 0x0a, 0x0c, 0x57, 0x4f, - 0x52, 0x4b, 0x45, 0x52, 0x5f, 0x52, 0x45, 0x50, 0x4c, 0x59, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, - 0x57, 0x4f, 0x52, 0x4b, 0x45, 0x52, 0x5f, 0x50, 0x41, 0x55, 0x53, 0x45, 0x10, 0x07, 0x2a, 0x3b, - 0x0a, 0x11, 0x53, 0x44, 0x4b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x11, 0x0a, 0x0d, 0x4e, 0x4f, 0x54, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, - 0x45, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x4e, 0x45, 0x10, 0x01, - 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x2a, 0x4c, 0x0a, 0x10, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, - 0x0a, 0x05, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x52, 0x41, - 0x49, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x49, 0x53, 0x43, 0x4f, - 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x03, 0x2a, 0x2d, 0x0a, 0x16, 0x57, 0x6f, 0x72, - 0x6b, 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x52, 0x65, 0x61, - 0x73, 0x6f, 0x6e, 0x12, 0x13, 0x0a, 0x0f, 0x57, 0x4f, 0x52, 0x4b, 0x45, 0x52, 0x5f, 0x53, 0x48, - 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x6e, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x2f, 0x69, - 0x6e, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5e, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x32, 0x0a, 0x04, 0x6b, 0x69, 0x6e, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, + 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x86, 0x01, 0x0a, 0x11, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1f, 0x0a, + 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x23, + 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, + 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, + 0x22, 0x73, 0x0a, 0x0e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, + 0x6c, 0x73, 0x12, 0x3c, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, + 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x48, 0x61, 0x73, 0x68, 0x22, 0x70, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, + 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x63, 0x61, + 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x75, + 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x66, + 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x70, 0x69, 0x5f, + 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x70, + 0x69, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0x6e, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x44, + 0x61, 0x74, 0x61, 0x12, 0x2c, 0x0a, 0x12, 0x68, 0x61, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x73, 0x69, + 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x10, 0x68, 0x61, 0x73, 0x68, 0x65, 0x64, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, + 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, + 0x12, 0x15, 0x0a, 0x06, 0x65, 0x6e, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x6e, 0x76, 0x49, 0x64, 0x22, 0xaa, 0x04, 0x0a, 0x18, 0x57, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x44, 0x61, 0x74, 0x61, 0x12, 0x3c, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x49, 0x64, 0x12, 0x31, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, 0x52, 0x08, 0x61, 0x75, 0x74, + 0x68, 0x44, 0x61, 0x74, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x31, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x3d, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x6d, 0x61, + 0x6e, 0x75, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x65, 0x73, 0x73, 0x5f, 0x61, + 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, + 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x65, 0x73, 0x73, 0x41, + 0x63, 0x6b, 0x12, 0x49, 0x0a, 0x11, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, + 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x10, 0x73, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, + 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, + 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, + 0x72, 0x6b, 0x12, 0x1f, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, + 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x64, 0x6b, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x64, 0x6b, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x64, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x67, + 0x75, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x64, 0x6b, 0x4c, + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x70, 0x6c, 0x61, 0x74, + 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x48, 0x0a, 0x16, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x53, + 0x79, 0x6e, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x20, + 0x0a, 0x09, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x08, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x64, 0x88, 0x01, 0x01, + 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x5f, 0x69, 0x64, 0x22, 0xe1, + 0x01, 0x0a, 0x1a, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, + 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, + 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, + 0x65, 0x6e, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6e, + 0x76, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x70, 0x70, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x75, + 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6c, 0x75, 0x67, 0x12, + 0x1c, 0x0a, 0x07, 0x73, 0x74, 0x65, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x65, 0x70, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x27, 0x0a, + 0x0f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x50, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x74, 0x65, 0x70, 0x5f, + 0x69, 0x64, 0x22, 0x9b, 0x01, 0x0a, 0x14, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x41, 0x63, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x70, + 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x70, 0x70, 0x49, + 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x6c, + 0x75, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x6c, 0x75, 0x67, 0x12, 0x1c, 0x0a, 0x07, 0x73, 0x74, 0x65, 0x70, 0x5f, 0x69, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x65, 0x70, 0x49, + 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x74, 0x65, 0x70, 0x5f, 0x69, 0x64, + 0x22, 0xe9, 0x01, 0x0a, 0x0b, 0x53, 0x44, 0x4b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, + 0x35, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x44, 0x4b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, + 0x5f, 0x72, 0x65, 0x74, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, + 0x52, 0x65, 0x74, 0x72, 0x79, 0x12, 0x24, 0x0a, 0x0b, 0x72, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x61, + 0x66, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x72, 0x65, + 0x74, 0x72, 0x79, 0x41, 0x66, 0x74, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0b, 0x73, + 0x64, 0x6b, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x73, 0x64, 0x6b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x0e, 0x0a, 0x0c, + 0x5f, 0x72, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x22, 0xeb, 0x02, 0x0a, + 0x0c, 0x43, 0x6f, 0x6e, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, + 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, + 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, + 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x65, 0x61, 0x72, 0x74, + 0x62, 0x65, 0x61, 0x74, 0x41, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, + 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, + 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x0a, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0x5c, 0x0a, 0x10, 0x53, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, + 0x0a, 0x09, 0x63, 0x70, 0x75, 0x5f, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, + 0x65, 0x6d, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x6d, 0x65, 0x6d, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x09, 0x43, 0x6f, 0x6e, + 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x15, 0x0a, 0x06, 0x65, 0x6e, 0x76, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6e, 0x76, 0x49, 0x64, 0x12, 0x15, 0x0a, + 0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, + 0x70, 0x70, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x2e, 0x0a, 0x05, 0x63, 0x6f, 0x6e, 0x6e, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x05, 0x63, 0x6f, 0x6e, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x07, 0x73, 0x79, 0x6e, 0x63, + 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x73, 0x79, 0x6e, + 0x63, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x1e, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, + 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, + 0x69, 0x64, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x2a, + 0xf0, 0x01, 0x0a, 0x12, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, + 0x59, 0x5f, 0x48, 0x45, 0x4c, 0x4c, 0x4f, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x57, 0x4f, 0x52, + 0x4b, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x01, 0x12, 0x1c, 0x0a, + 0x18, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x02, 0x12, 0x1c, 0x0a, 0x18, 0x47, + 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x5f, 0x45, 0x58, 0x45, 0x43, 0x55, 0x54, 0x4f, 0x52, 0x5f, + 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x57, 0x4f, 0x52, + 0x4b, 0x45, 0x52, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x04, 0x12, 0x16, 0x0a, 0x12, 0x57, + 0x4f, 0x52, 0x4b, 0x45, 0x52, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x5f, 0x41, 0x43, + 0x4b, 0x10, 0x05, 0x12, 0x10, 0x0a, 0x0c, 0x57, 0x4f, 0x52, 0x4b, 0x45, 0x52, 0x5f, 0x52, 0x45, + 0x50, 0x4c, 0x59, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x57, 0x4f, 0x52, 0x4b, 0x45, 0x52, 0x5f, + 0x50, 0x41, 0x55, 0x53, 0x45, 0x10, 0x07, 0x12, 0x14, 0x0a, 0x10, 0x57, 0x4f, 0x52, 0x4b, 0x45, + 0x52, 0x5f, 0x48, 0x45, 0x41, 0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x08, 0x12, 0x13, 0x0a, + 0x0f, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x49, 0x4e, 0x47, + 0x10, 0x09, 0x2a, 0x3b, 0x0a, 0x11, 0x53, 0x44, 0x4b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x11, 0x0a, 0x0d, 0x4e, 0x4f, 0x54, 0x5f, 0x43, + 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, + 0x4e, 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x2a, + 0x4c, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x01, 0x12, 0x0c, 0x0a, + 0x08, 0x44, 0x52, 0x41, 0x49, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x44, + 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x03, 0x2a, 0x2d, 0x0a, + 0x16, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x13, 0x0a, 0x0f, 0x57, 0x4f, 0x52, 0x4b, 0x45, + 0x52, 0x5f, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x42, 0x39, 0x5a, 0x37, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x6e, 0x6e, 0x67, 0x65, + 0x73, 0x74, 0x2f, 0x69, 0x6e, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x76, 0x31, 0x3b, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1394,6 +1418,7 @@ var file_connect_v1_connect_proto_goTypes = []any{ (*ConnMetadata)(nil), // 14: connect.v1.ConnMetadata (*SystemAttributes)(nil), // 15: connect.v1.SystemAttributes (*ConnGroup)(nil), // 16: connect.v1.ConnGroup + (*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp } var file_connect_v1_connect_proto_depIdxs = []int32{ 0, // 0: connect.v1.ConnectMessage.kind:type_name -> connect.v1.GatewayMessageType @@ -1404,13 +1429,14 @@ var file_connect_v1_connect_proto_depIdxs = []int32{ 15, // 5: connect.v1.WorkerConnectRequestData.system_attributes:type_name -> connect.v1.SystemAttributes 1, // 6: connect.v1.SDKResponse.status:type_name -> connect.v1.SDKResponseStatus 2, // 7: connect.v1.ConnMetadata.status:type_name -> connect.v1.ConnectionStatus - 15, // 8: connect.v1.ConnMetadata.attributes:type_name -> connect.v1.SystemAttributes - 14, // 9: connect.v1.ConnGroup.conns:type_name -> connect.v1.ConnMetadata - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 17, // 8: connect.v1.ConnMetadata.last_heartbeat_at:type_name -> google.protobuf.Timestamp + 15, // 9: connect.v1.ConnMetadata.attributes:type_name -> connect.v1.SystemAttributes + 14, // 10: connect.v1.ConnGroup.conns:type_name -> connect.v1.ConnMetadata + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_connect_v1_connect_proto_init() } diff --git a/vendor/modules.txt b/vendor/modules.txt index bb646e4e..9c3d5c7b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,10 @@ +# github.com/99designs/gqlgen v0.17.27 +## explicit; go 1.18 # github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da ## explicit github.com/aead/chacha20/chacha +# github.com/agnivade/levenshtein v1.1.1 +## explicit; go 1.13 # github.com/antlr4-go/antlr/v4 v4.13.1 ## explicit; go 1.22 github.com/antlr4-go/antlr/v4 @@ -21,9 +25,13 @@ github.com/coder/websocket/internal/errd github.com/coder/websocket/internal/util github.com/coder/websocket/internal/wsjs github.com/coder/websocket/internal/xsync +# github.com/cpuguy83/go-md2man/v2 v2.0.4 +## explicit; go 1.11 # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew +# github.com/dmarkham/enumer v1.5.8 +## explicit; go 1.17 # github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 ## explicit github.com/dustinkirkland/golang-petname @@ -100,7 +108,7 @@ github.com/hashicorp/hcl/json/token # github.com/inngest/expr v0.0.0-20241106234328-863dff7deec0 ## explicit; go 1.23.2 github.com/inngest/expr -# github.com/inngest/inngest v1.2.1-0.20241120122427-5ae5a84711e0 +# github.com/inngest/inngest v1.2.1-0.20241121162138-fa921e44acf1 ## explicit; go 1.23.2 github.com/inngest/inngest/pkg/connect/types github.com/inngest/inngest/pkg/connect/wsproto @@ -162,6 +170,8 @@ github.com/ohler55/ojg/jp # github.com/oklog/ulid/v2 v2.1.0 ## explicit; go 1.15 github.com/oklog/ulid/v2 +# github.com/pascaldekloe/name v1.0.1 +## explicit; go 1.10 # github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 ## explicit; go 1.16 github.com/pbnjay/memory @@ -199,6 +209,8 @@ github.com/robfig/cron/v3 github.com/rs/zerolog github.com/rs/zerolog/internal/cbor github.com/rs/zerolog/internal/json +# github.com/russross/blackfriday/v2 v2.1.0 +## explicit # github.com/sashabaranov/go-openai v1.35.6 ## explicit; go 1.18 github.com/sashabaranov/go-openai @@ -243,9 +255,15 @@ github.com/subosito/gotenv # github.com/tidwall/btree v1.7.0 ## explicit; go 1.19 github.com/tidwall/btree +# github.com/urfave/cli/v2 v2.25.1 +## explicit; go 1.18 +# github.com/vektah/gqlparser/v2 v2.5.15 +## explicit; go 1.19 # github.com/xhit/go-str2duration/v2 v2.1.0 ## explicit; go 1.13 github.com/xhit/go-str2duration/v2 +# github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 +## explicit # go.opentelemetry.io/otel v1.28.0 ## explicit; go 1.21 go.opentelemetry.io/otel @@ -357,6 +375,8 @@ golang.org/x/exp/slices golang.org/x/exp/slog golang.org/x/exp/slog/internal golang.org/x/exp/slog/internal/buffer +# golang.org/x/mod v0.21.0 +## explicit; go 1.22.0 # golang.org/x/net v0.30.0 ## explicit; go 1.18 golang.org/x/net/http/httpguts @@ -390,6 +410,8 @@ golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm +# golang.org/x/tools v0.26.0 +## explicit; go 1.22.0 # google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 ## explicit; go 1.21 google.golang.org/genproto/googleapis/api/expr/v1alpha1 From f3ee65a64a2c74082db720fa16f60bc34d16dd68 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Thu, 21 Nov 2024 19:06:45 +0100 Subject: [PATCH 04/16] send client heartbeats --- connect.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/connect.go b/connect.go index 47ab1b4c..5eb5788e 100644 --- a/connect.go +++ b/connect.go @@ -30,6 +30,10 @@ import ( "time" ) +const ( + WorkerHeartbeatInterval = 10 * time.Second +) + type workerPoolMsg struct { msg *connectproto.ConnectMessage ws *websocket.Conn @@ -504,6 +508,25 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs } } + go func() { + heartbeatTicker := time.NewTicker(WorkerHeartbeatInterval) + defer heartbeatTicker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-heartbeatTicker.C: + err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_HEARTBEAT, + }) + if err != nil { + h.h.Logger.Error("failed to send worker heartbeat", "err", err) + } + } + + } + }() + eg := errgroup.Group{} eg.Go(func() error { for { From 0fd900b4ede4cb088169362a26e3fe88850d2c42 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 14:43:12 +0100 Subject: [PATCH 05/16] Split out connect logic into separate pkg --- connect.go | 934 ++------------------------------------- connect/gateway_hosts.go | 59 +++ connect/handler.go | 854 +++++++++++++++++++++++++++++++++++ connect/worker_pool.go | 62 +++ handler.go | 1 - 5 files changed, 1022 insertions(+), 888 deletions(-) create mode 100644 connect/gateway_hosts.go create mode 100644 connect/handler.go create mode 100644 connect/worker_pool.go diff --git a/connect.go b/connect.go index 5eb5788e..069d1649 100644 --- a/connect.go +++ b/connect.go @@ -2,936 +2,96 @@ package inngestgo import ( "context" - "crypto/rand" - "encoding/json" - "errors" "fmt" - "github.com/coder/websocket" - "github.com/inngest/inngest/pkg/connect/types" - "github.com/inngest/inngest/pkg/connect/wsproto" - "github.com/inngest/inngest/pkg/enums" + "github.com/inngest/inngest/pkg/execution/state" "github.com/inngest/inngest/pkg/publicerr" - "github.com/inngest/inngest/pkg/syscode" - connectproto "github.com/inngest/inngest/proto/gen/connect/v1" - sdkerrors "github.com/inngest/inngestgo/errors" - "github.com/pbnjay/memory" - "golang.org/x/sync/errgroup" - "google.golang.org/protobuf/proto" - "io" - "net" - "net/url" - "runtime" - "sync" - + "github.com/inngest/inngestgo/connect" "github.com/inngest/inngestgo/internal/sdkrequest" - "github.com/oklog/ulid/v2" - "os" - "strings" - "time" -) - -const ( - WorkerHeartbeatInterval = 10 * time.Second + "net/url" ) -type workerPoolMsg struct { - msg *connectproto.ConnectMessage - ws *websocket.Conn -} - -type connectHandler struct { - h *handler - - connectionId ulid.ULID - - messageBuffer []*connectproto.ConnectMessage - messageBufferLock sync.Mutex - - inProgress sync.WaitGroup - - workerPoolMsgs chan workerPoolMsg - - hostsManager *hostsManager -} - -type hostsManager struct { - gatewayHosts []string - availableGatewayHosts map[string]struct{} - drainingGatewayHosts map[string]struct{} - unreachableGatewayHosts map[string]struct{} - hostsLock sync.RWMutex -} - -func newHostsManager(gatewayHosts []string) *hostsManager { - hm := &hostsManager{ - gatewayHosts: gatewayHosts, - availableGatewayHosts: make(map[string]struct{}), - drainingGatewayHosts: make(map[string]struct{}), - unreachableGatewayHosts: make(map[string]struct{}), - } - - hm.resetGateways() - - return hm -} - -func (h *hostsManager) pickAvailableGateway() string { - h.hostsLock.RLock() - defer h.hostsLock.RUnlock() - - for host := range h.availableGatewayHosts { - return host - } - return "" -} - -func (h *hostsManager) markDrainingGateway(host string) { - h.hostsLock.Lock() - defer h.hostsLock.Unlock() - delete(h.availableGatewayHosts, host) - h.drainingGatewayHosts[host] = struct{}{} -} - -func (h *hostsManager) markUnreachableGateway(host string) { - h.hostsLock.Lock() - defer h.hostsLock.Unlock() - delete(h.availableGatewayHosts, host) - h.unreachableGatewayHosts[host] = struct{}{} -} - -func (h *hostsManager) resetGateways() { - h.hostsLock.Lock() - defer h.hostsLock.Unlock() - h.availableGatewayHosts = make(map[string]struct{}) - h.drainingGatewayHosts = make(map[string]struct{}) - h.unreachableGatewayHosts = make(map[string]struct{}) - for _, host := range h.gatewayHosts { - h.availableGatewayHosts[host] = struct{}{} - } -} - -// authContext is wrapper for information related to authentication -type authContext struct { - signingKey string - fallback bool -} - -func (h *connectHandler) connectURLs() []string { - if h.h.isDev() { - return []string{fmt.Sprintf("%s/connect", strings.Replace(DevServerURL(), "http", "ws", 1))} - } - - if len(h.h.ConnectURLs) > 0 { - return h.h.ConnectURLs - } - - return nil -} - func (h *handler) Connect(ctx context.Context) error { - h.useConnect = true concurrency := h.HandlerOpts.GetWorkerConcurrency() - // This determines how many messages can be processed by each worker at once. - ch := connectHandler{ - h: h, - - // Should this use the same buffer size as the worker pool? - workerPoolMsgs: make(chan workerPoolMsg, concurrency), - } - - for i := 0; i < concurrency; i++ { - go ch.workerPool(ctx) - } - - defer func() { - // TODO Push remaining messages to another destination for processing? - }() - - err := ch.Connect(ctx) - if err != nil { - if ctx.Err() != nil { - return nil - } - - return fmt.Errorf("could not establish connection: %w", err) - } - - return nil -} - -func (h *connectHandler) instanceId() string { - if h.h.InstanceId != nil { - return *h.h.InstanceId - } - - hostname, _ := os.Hostname() - if hostname != "" { - return hostname - } - - // TODO Is there any stable identifier that can be used as a fallback? - return "" -} - -func (h *connectHandler) workerPool(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case msg := <-h.workerPoolMsgs: - h.processExecutorRequest(msg.ws, msg.msg) - } - } -} - -func (h *connectHandler) Connect(ctx context.Context) error { - signingKey := h.h.GetSigningKey() - if signingKey == "" { - return fmt.Errorf("must provide signing key") - } - auth := authContext{signingKey: signingKey} - - numCpuCores := runtime.NumCPU() - totalMem := memory.TotalMemory() - connectPlaceholder := url.URL{ Scheme: "ws", Host: "connect", } - fns, err := createFunctionConfigs(h.h.appName, h.h.funcs, connectPlaceholder, true) + fns, err := createFunctionConfigs(h.appName, h.funcs, connectPlaceholder, true) if err != nil { return fmt.Errorf("error creating function configs: %w", err) } - marshaledFns, err := json.Marshal(fns) - if err != nil { - return fmt.Errorf("failed to serialize connect config: %w", err) - } - - marshaledCapabilities, err := json.Marshal(capabilities) - if err != nil { - return fmt.Errorf("failed to serialize connect config: %w", err) - } - - hosts := h.connectURLs() - if len(hosts) == 0 { - return fmt.Errorf("no connect URLs provided") - } - - h.hostsManager = newHostsManager(hosts) - - eg := errgroup.Group{} - - notifyConnectDoneChan := make(chan connectReport) - notifyConnectedChan := make(chan struct{}) - initiateConnectionChan := make(chan struct{}) - - var attempts int - eg.Go(func() error { - for { - select { - case <-ctx.Done(): - return nil - case <-notifyConnectedChan: - attempts = 0 - continue - case msg := <-notifyConnectDoneChan: - h.h.Logger.Error("connect failed", "err", err, "reconnect", msg.reconnect) - - if !msg.reconnect { - return err - } - - if msg.err != nil { - closeErr := websocket.CloseError{} - if errors.As(err, &closeErr) { - switch closeErr.Reason { - // If auth failed, retry with fallback key - case syscode.CodeConnectAuthFailed: - if auth.fallback { - return fmt.Errorf("failed to authenticate with fallback key, exiting") - } - - signingKeyFallback := h.h.GetSigningKeyFallback() - if signingKeyFallback != "" { - auth = authContext{signingKey: signingKeyFallback, fallback: true} - } - - initiateConnectionChan <- struct{}{} - - continue - - // Retry on the following error codes - case syscode.CodeConnectGatewayClosing, syscode.CodeConnectInternal, syscode.CodeConnectWorkerHelloTimeout: - initiateConnectionChan <- struct{}{} - - continue - - default: - // If we received a reason that's non-retriable, stop here. - return fmt.Errorf("connect failed with error code %q", closeErr.Reason) - } - } - } - - initiateConnectionChan <- struct{}{} - case <-initiateConnectionChan: - } - - if attempts == 5 { - return fmt.Errorf("could not establish connection after 5 attempts") - } - - attempts++ - - h.connect(ctx, false, connectionEstablishData{ - signingKey: auth.signingKey, - numCpuCores: int32(numCpuCores), - totalMem: int64(totalMem), - marshaledFns: marshaledFns, - marshaledCapabilities: marshaledCapabilities, - }, notifyConnectedChan, notifyConnectDoneChan) - } - }) - - initiateConnectionChan <- struct{}{} - - if err := eg.Wait(); err != nil { - return fmt.Errorf("could not establish connection: %w", err) - } - - // Send out buffered messages, using new connection if necessary! - h.messageBufferLock.Lock() - defer h.messageBufferLock.Unlock() - if len(h.messageBuffer) > 0 { - // Send buffered messages via a working connection - err = h.withTemporaryConnection(connectionEstablishData{ - signingKey: auth.signingKey, - numCpuCores: int32(numCpuCores), - totalMem: int64(totalMem), - marshaledFns: marshaledFns, - marshaledCapabilities: marshaledCapabilities, - }, func(ws *websocket.Conn) error { - // Send buffered messages - err := h.sendBufferedMessages(ws) - if err != nil { - return fmt.Errorf("could not send buffered messages: %w", err) - } - - return nil - }) - if err != nil { - h.h.Logger.Error("could not establish connection for sending buffered messages", "err", err) - } - - // TODO Push remaining messages to another destination for processing? - } - - return nil -} - -type connectionEstablishData struct { - signingKey string - numCpuCores int32 - totalMem int64 - marshaledFns []byte - marshaledCapabilities []byte - manualReadinessAck bool -} - -type preparedConnection struct { - ws *websocket.Conn - gatewayHost string -} - -func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGateways bool, data connectionEstablishData) (preparedConnection, bool, error) { - connectTimeout, cancelConnectTimeout := context.WithTimeout(ctx, 10*time.Second) - defer cancelConnectTimeout() - - gatewayHost := h.hostsManager.pickAvailableGateway() - if gatewayHost == "" { - h.hostsManager.resetGateways() - - return preparedConnection{}, allowResettingGateways, fmt.Errorf("no available gateway hosts") + signingKey := h.GetSigningKey() + if signingKey == "" { + return fmt.Errorf("signing key is required") } - // Establish WebSocket connection to one of the gateways - ws, _, err := websocket.Dial(connectTimeout, gatewayHost, &websocket.DialOptions{ - Subprotocols: []string{ - types.GatewaySubProtocol, - }, - }) + hashedKey, err := hashedSigningKey([]byte(signingKey)) if err != nil { - h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, false, fmt.Errorf("could not connect to gateway: %w", err) - } - - // Connection ID is unique per connection, reconnections should get a new ID - h.connectionId = ulid.MustNew(ulid.Now(), rand.Reader) - - h.h.Logger.Debug("websocket connection established") - - // Wait for gateway hello message - { - initialMessageTimeout, cancelInitialTimeout := context.WithTimeout(ctx, 5*time.Second) - defer cancelInitialTimeout() - var helloMessage connectproto.ConnectMessage - err = wsproto.Read(initialMessageTimeout, ws, &helloMessage) - if err != nil { - h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, true, fmt.Errorf("did not receive gateway hello message: %w", err) - } - - if helloMessage.Kind != connectproto.GatewayMessageType_GATEWAY_HELLO { - h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, true, fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind) - } - - h.h.Logger.Debug("received gateway hello message") + return fmt.Errorf("failed to hash signing key: %w", err) } - // Send connect message + var hashedFallbackKey []byte { - hashedKey, err := hashedSigningKey([]byte(data.signingKey)) - if err != nil { - return preparedConnection{}, false, fmt.Errorf("could not hash signing key: %w", err) - } - - apiOrigin := h.h.GetAPIBaseURL() - if h.h.isDev() { - apiOrigin = DevServerURL() - } - - data, err := proto.Marshal(&connectproto.WorkerConnectRequestData{ - SessionId: &connectproto.SessionIdentifier{ - BuildId: h.h.BuildId, - InstanceId: h.instanceId(), - ConnectionId: h.connectionId.String(), - }, - AuthData: &connectproto.AuthData{ - HashedSigningKey: hashedKey, - }, - AppName: h.h.appName, - Config: &connectproto.ConfigDetails{ - Capabilities: data.marshaledCapabilities, - Functions: data.marshaledFns, - ApiOrigin: apiOrigin, - }, - SystemAttributes: &connectproto.SystemAttributes{ - CpuCores: data.numCpuCores, - MemBytes: data.totalMem, - Os: runtime.GOOS, - }, - Environment: h.h.Env, - Platform: Ptr(platform()), - SdkVersion: SDKVersion, - SdkLanguage: SDKLanguage, - WorkerManualReadinessAck: data.manualReadinessAck, - }) - if err != nil { - return preparedConnection{}, false, fmt.Errorf("could not serialize sdk connect message: %w", err) - } - - err = wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_CONNECT, - Payload: data, - }) - if err != nil { - return preparedConnection{}, true, fmt.Errorf("could not send initial message") - } - } - - // Wait for gateway ready message - { - connectionReadyTimeout, cancelConnectionReadyTimeout := context.WithTimeout(ctx, 20*time.Second) - defer cancelConnectionReadyTimeout() - var connectionReadyMsg connectproto.ConnectMessage - err = wsproto.Read(connectionReadyTimeout, ws, &connectionReadyMsg) - if err != nil { - return preparedConnection{}, true, fmt.Errorf("did not receive gateway connection ready message: %w", err) - } - - if connectionReadyMsg.Kind != connectproto.GatewayMessageType_GATEWAY_CONNECTION_READY { - return preparedConnection{}, true, fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind) - } - - h.h.Logger.Debug("received gateway connection ready message") - } - - return preparedConnection{ws, gatewayHost}, false, nil -} - -func (h *connectHandler) sendBufferedMessages(ws *websocket.Conn) error { - processed := 0 - for _, msg := range h.messageBuffer { - // always send the message, even if the context is cancelled - err := wsproto.Write(context.Background(), ws, msg) - if err != nil { - // Only send buffered messages once - h.messageBuffer = h.messageBuffer[processed:] - - h.h.Logger.Error("failed to send buffered message", "err", err) - return fmt.Errorf("could not send buffered message: %w", err) - } - - h.h.Logger.Debug("sent buffered message", "msg", msg) - processed++ - } - h.messageBuffer = nil - return nil -} - -var errGatewayDraining = errors.New("gateway is draining") - -func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) (reconnect bool, err error) { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - defer func() { - // TODO Do we need to include a reason here? If we only use this for unexpected disconnects, probably not - _ = ws.CloseNow() - }() - - go func() { - <-ctx.Done() - _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) - }() - - // Send buffered but unsent messages if connection was re-established - if len(h.messageBuffer) > 0 { - h.h.Logger.Debug("sending buffered messages", "count", len(h.messageBuffer)) - err = h.sendBufferedMessages(ws) - if err != nil { - return true, fmt.Errorf("could not send buffered messages: %w", err) - } - } - - go func() { - heartbeatTicker := time.NewTicker(WorkerHeartbeatInterval) - defer heartbeatTicker.Stop() - for { - select { - case <-ctx.Done(): - return - case <-heartbeatTicker.C: - err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_HEARTBEAT, - }) - if err != nil { - h.h.Logger.Error("failed to send worker heartbeat", "err", err) - } - } - - } - }() - - eg := errgroup.Group{} - eg.Go(func() error { - for { - var msg connectproto.ConnectMessage - err = wsproto.Read(context.Background(), ws, &msg) + if fallbackKey := h.GetSigningKeyFallback(); fallbackKey != "" { + hashedFallbackKey, err = hashedSigningKey([]byte(fallbackKey)) if err != nil { - h.h.Logger.Error("failed to read message", "err", err) - - // The connection may still be active, but for some reason we couldn't read the message - return err + return fmt.Errorf("failed to hash fallback signing key: %w", err) } - - h.h.Logger.Debug("received gateway request", "msg", &msg) - - switch msg.Kind { - case connectproto.GatewayMessageType_GATEWAY_CLOSING: - // Stop the read loop: We will not receive any further messages and should establish a new connection - // We can still use the old connection to send replies to the gateway - return errGatewayDraining - case connectproto.GatewayMessageType_GATEWAY_EXECUTOR_REQUEST: - // Handle invoke in a non-blocking way to allow for other messages to be processed - h.inProgress.Add(1) - h.workerPoolMsgs <- workerPoolMsg{ - msg: &msg, - ws: ws, - } - default: - h.h.Logger.Error("got unknown gateway request", "err", err) - continue - } - } - }) - - h.h.Logger.Debug("waiting for read loop to end") - - // If read loop ends, this can be for two reasons - // - Connection loss (io.EOF), read loop terminated intentionally (CloseError), other error (unexpected) - // - Worker shutdown, parent context got cancelled - if err := eg.Wait(); err != nil && ctx.Err() == nil { - if errors.Is(err, errGatewayDraining) { - h.hostsManager.markDrainingGateway(gatewayHost) - - // Gateway is draining and will not accept new connections. - // We must reconnect to a different gateway, only then can we close the old connection. - waitUntilConnected, doneWaiting := context.WithCancel(context.Background()) - defer doneWaiting() - - // Intercept connected signal and pass it to the main goroutine - notifyConnectedInterceptChan := make(chan struct{}) - go func() { - <-notifyConnectedChan - notifyConnectedInterceptChan <- struct{}{} - doneWaiting() - }() - - // Establish new connection and pass close reports back to the main goroutine - h.connect(ctx, true, data, notifyConnectedInterceptChan, notifyConnectDoneChan) - - // Wait until the new connection is established before closing the old one - <-waitUntilConnected.Done() - - // By returning, we will close the old connection - return false, errGatewayDraining - } - - h.h.Logger.Debug("read loop ended with error", "err", err) - - // In case the gateway intentionally closed the connection, we'll receive a close error - cerr := websocket.CloseError{} - if errors.As(err, &cerr) { - h.h.Logger.Error("connection closed with reason", "reason", cerr.Reason) - - // Reconnect! - return true, fmt.Errorf("connection closed with reason %q: %w", cerr.Reason, cerr) - } - - // connection closed without reason - if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { - h.h.Logger.Error("failed to read message from gateway, lost connection unexpectedly", "err", err) - return true, fmt.Errorf("connection closed unexpectedly: %w", cerr) - } - - // If this is not a worker shutdown, we should reconnect - return true, fmt.Errorf("connection closed unexpectedly: %w", ctx.Err()) - } - - // Perform graceful shutdown routine (context was cancelled) - - // Signal gateway that we won't process additional messages! - { - h.h.Logger.Debug("sending worker pause message") - err := wsproto.Write(context.Background(), ws, &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_PAUSE, - }) - if err != nil { - // We should not exit here, as we're already in the shutdown routine - h.h.Logger.Error("failed to serialize worker pause msg", "err", err) - } - } - - h.h.Logger.Debug("waiting for in-progress requests to finish") - - // Wait until all in-progress requests are completed - h.inProgress.Wait() - - // Attempt to shut down connection if not already done - _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) - - return false, nil -} - -type connectReport struct { - reconnect bool - err error -} - -func (h *connectHandler) connect(ctx context.Context, allowResettingGateways bool, data connectionEstablishData, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) { - preparedConn, reconnect, err := h.prepareConnection(ctx, allowResettingGateways, data) - if err != nil { - notifyConnectDoneChan <- connectReport{ - reconnect: reconnect, - err: fmt.Errorf("could not establish connection: %w", err), - } - return - } - - notifyConnectedChan <- struct{}{} - - reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost, notifyConnectedChan, notifyConnectDoneChan) - if err != nil { - if errors.Is(err, errGatewayDraining) { - // if the gateway is draining, the original connection was closed, and we already reconnected inside handleConnection - return - } - - notifyConnectDoneChan <- connectReport{ - reconnect: reconnect, - err: fmt.Errorf("could not handle connection: %w", err), - } - return - } - - notifyConnectDoneChan <- connectReport{ - reconnect: reconnect, - err: nil, - } -} - -func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, handler func(ws *websocket.Conn) error) error { - // Prevent this connection from receiving work - data.manualReadinessAck = true - - maxAttempts := 4 - - var conn *websocket.Conn - var attempts int - for { - if attempts == maxAttempts { - return fmt.Errorf("could not establish connection after %d attempts", maxAttempts) - } - - ws, _, err := h.prepareConnection(context.Background(), true, data) - if err != nil { - attempts++ - continue } - - conn = ws.ws - break } - defer func() { - _ = conn.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) - }() - - err := handler(conn) - if err != nil { - return err - } - - return nil -} - -func (h *connectHandler) processExecutorRequest(ws *websocket.Conn, msg *connectproto.ConnectMessage) { - defer h.inProgress.Done() - - // Always make sure the invoke finishes properly - processCtx := context.Background() - - err := h.handleInvokeMessage(processCtx, ws, msg) - - // When we encounter an error, we cannot retry the connection from inside the goroutine. - // If we're dealing with connection loss, the next read loop will fail with the same error - // and handle the reconnection. - if err != nil { - cerr := websocket.CloseError{} - if errors.As(err, &cerr) { - h.h.Logger.Error("gateway connection closed with reason", "reason", cerr.Reason) - return - } - - if errors.Is(err, io.EOF) { - h.h.Logger.Error("gateway connection closed unexpectedly", "err", err) - return - } - - // TODO If error is not connection-related, should we retry? Send the buffered message? - } -} - -func (h *connectHandler) handleInvokeMessage(ctx context.Context, ws *websocket.Conn, msg *connectproto.ConnectMessage) error { - resp, err := h.connectInvoke(ctx, ws, msg) - if err != nil { - h.h.Logger.Error("failed to handle sdk request", "err", err) - // TODO Should we drop the connection? Continue receiving messages? - return fmt.Errorf("could not handle sdk request: %w", err) - } - - data, err := proto.Marshal(resp) - if err != nil { - h.h.Logger.Error("failed to serialize sdk response", "err", err) - // TODO This should never happen; Signal that we should retry - return fmt.Errorf("could not serialize sdk response: %w", err) - } - - responseMessage := &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_REPLY, - Payload: data, - } - - err = wsproto.Write(ctx, ws, responseMessage) - if err != nil { - h.h.Logger.Error("failed to send sdk response", "err", err) - - // Buffer message to retry - h.messageBufferLock.Lock() - h.messageBuffer = append(h.messageBuffer, responseMessage) - h.messageBufferLock.Unlock() - - return fmt.Errorf("could not send sdk response: %w", err) - } - - return nil -} - -// connectInvoke is the counterpart to invoke for connect -func (h *connectHandler) connectInvoke(ctx context.Context, ws *websocket.Conn, msg *connectproto.ConnectMessage) (*connectproto.SDKResponse, error) { - body := connectproto.GatewayExecutorRequestData{} - if err := proto.Unmarshal(msg.Payload, &body); err != nil { - // TODO Should we send this back to the gateway? - h.h.Logger.Error("error decoding gateway request data", "error", err) - return nil, fmt.Errorf("invalid gateway message data: %w", err) - } - - // Note: This still uses JSON - // TODO Replace with Protobuf - var request sdkrequest.Request - if err := json.Unmarshal(body.RequestPayload, &request); err != nil { - // TODO Should we send this back to the gateway? Previously this was a status code 400 public error with "malformed input" - h.h.Logger.Error("error decoding sdk request", "error", err) - return nil, fmt.Errorf("invalid SDK request payload: %w", err) - } - - ackPayload, err := proto.Marshal(&connectproto.WorkerRequestAckData{ - RequestId: body.RequestId, - AppId: body.AppId, - FunctionSlug: body.FunctionSlug, - StepId: body.StepId, - }) - if err != nil { - h.h.Logger.Error("error marshaling request ack", "error", err) - return nil, publicerr.Error{ - Message: "malformed input", - Status: 400, - } - } - - // Ack message - // If we're shutting down (context is canceled) we will not ack, which is desired! - if err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_REQUEST_ACK, - Payload: ackPayload, - }); err != nil { - h.h.Logger.Error("error sending request ack", "error", err) - return nil, publicerr.Error{ - Message: "failed to ack worker request", - Status: 400, - } - } - - // TODO Should we wait for a gateway response before starting to process? What if the gateway fails acking and we start too early? - // This should not happen but could lead to double processing of the same message - - if request.UseAPI { - // TODO: implement this - // retrieve data from API - // request.Steps = - // request.Events = - _ = 0 // no-op to avoid linter error - } - - h.h.l.RLock() + return connect.Connect(ctx, connect.Opts{ + AppName: h.appName, + Env: h.Env, + Functions: fns, + Capabilities: capabilities, + HashedSigningKey: hashedKey, + HashedSigningKeyFallback: hashedFallbackKey, + WorkerConcurrency: concurrency, + APIBaseUrl: h.GetAPIBaseURL(), + IsDev: h.isDev(), + DevServerUrl: DevServerURL(), + ConnectUrls: h.ConnectURLs, + InstanceId: h.InstanceId, + BuildId: h.BuildId, + Platform: Ptr(platform()), + SDKVersion: SDKVersion, + SDKLanguage: SDKLanguage, + }, h, h.Logger) +} + +func (h *handler) getServableFunctionBySlug(slug string) ServableFunction { + h.l.RLock() var fn ServableFunction - for _, f := range h.h.funcs { - if f.Slug() == body.FunctionSlug { + for _, f := range h.funcs { + if f.Slug() == slug { fn = f break } } - h.h.l.RUnlock() + h.l.RUnlock() + + return fn +} + +func (h *handler) InvokeFunction(ctx context.Context, slug string, stepId *string, request sdkrequest.Request) (any, []state.GeneratorOpcode, error) { + fn := h.getServableFunctionBySlug(slug) if fn == nil { // XXX: This is a 500 within the JS SDK. We should probably change // the JS SDK's status code to 410. 404 indicates that the overall // API for serving Inngest isn't found. - return nil, publicerr.Error{ - Message: fmt.Sprintf("function not found: %s", body.FunctionSlug), + return nil, nil, publicerr.Error{ + Message: fmt.Sprintf("function not found: %s", slug), Status: 410, } } - var stepId *string - if body.StepId != nil && *body.StepId != "step" { - stepId = body.StepId - } - // Invoke function, always complete regardless of resp, ops, err := invoke(context.Background(), fn, &request, stepId) - // NOTE: When triggering step errors, we should have an OpcodeStepError - // within ops alongside an error. We can safely ignore that error, as it's - // only used for checking whether the step used a NoRetryError or RetryAtError - // - // For that reason, we check those values first. - noRetry := sdkerrors.IsNoRetryError(err) - retryAt := sdkerrors.GetRetryAtTime(err) - if len(ops) == 1 && ops[0].Op == enums.OpcodeStepError { - // Now we've handled error types we can ignore step - // errors safely. - err = nil - } - - // Now that we've handled the OpcodeStepError, if we *still* ahve - // a StepError kind returned from a function we must have an unhandled - // step error. This is a NonRetryableError, as the most likely code is: - // - // _, err := step.Run(ctx, func() (any, error) { return fmt.Errorf("") }) - // if err != nil { - // return err - // } - if sdkerrors.IsStepError(err) { - err = fmt.Errorf("Unhandled step error: %s", err) - noRetry = true - } - - // These may be added even for 2xx codes with step errors. - var retryAfterVal *string - if retryAt != nil { - retryAfterVal = StrPtr(retryAt.Format(time.RFC3339)) - } - - if err != nil { - h.h.Logger.Error("error calling function", "error", err) - return &connectproto.SDKResponse{ - RequestId: body.RequestId, - Status: connectproto.SDKResponseStatus_ERROR, - Body: []byte(fmt.Sprintf("error calling function: %s", err.Error())), - NoRetry: noRetry, - RetryAfter: retryAfterVal, - }, nil - } - - if len(ops) > 0 { - // Note: This still uses JSON - // TODO Replace with Protobuf - serializedOps, err := json.Marshal(ops) - if err != nil { - return nil, fmt.Errorf("could not serialize ops: %w", err) - } - - // Return the function opcode returned here so that we can re-invoke this - // function and manage state appropriately. Any opcode here takes precedence - // over function return values as the function has not yet finished. - return &connectproto.SDKResponse{ - RequestId: body.RequestId, - Status: connectproto.SDKResponseStatus_NOT_COMPLETED, - Body: serializedOps, - NoRetry: noRetry, - RetryAfter: retryAfterVal, - }, nil - } - - // Note: This still uses JSON - // TODO Replace with Protobuf - serializedResp, err := json.Marshal(resp) - if err != nil { - return nil, fmt.Errorf("could not serialize resp: %w", err) - } - - // Return the function response. - return &connectproto.SDKResponse{ - RequestId: body.RequestId, - Status: connectproto.SDKResponseStatus_DONE, - Body: serializedResp, - NoRetry: noRetry, - RetryAfter: retryAfterVal, - }, nil + return resp, ops, err } diff --git a/connect/gateway_hosts.go b/connect/gateway_hosts.go new file mode 100644 index 00000000..73b0639c --- /dev/null +++ b/connect/gateway_hosts.go @@ -0,0 +1,59 @@ +package connect + +import "sync" + +type hostsManager struct { + gatewayHosts []string + availableGatewayHosts map[string]struct{} + drainingGatewayHosts map[string]struct{} + unreachableGatewayHosts map[string]struct{} + hostsLock sync.RWMutex +} + +func newHostsManager(gatewayHosts []string) *hostsManager { + hm := &hostsManager{ + gatewayHosts: gatewayHosts, + availableGatewayHosts: make(map[string]struct{}), + drainingGatewayHosts: make(map[string]struct{}), + unreachableGatewayHosts: make(map[string]struct{}), + } + + hm.resetGateways() + + return hm +} + +func (h *hostsManager) pickAvailableGateway() string { + h.hostsLock.RLock() + defer h.hostsLock.RUnlock() + + for host := range h.availableGatewayHosts { + return host + } + return "" +} + +func (h *hostsManager) markDrainingGateway(host string) { + h.hostsLock.Lock() + defer h.hostsLock.Unlock() + delete(h.availableGatewayHosts, host) + h.drainingGatewayHosts[host] = struct{}{} +} + +func (h *hostsManager) markUnreachableGateway(host string) { + h.hostsLock.Lock() + defer h.hostsLock.Unlock() + delete(h.availableGatewayHosts, host) + h.unreachableGatewayHosts[host] = struct{}{} +} + +func (h *hostsManager) resetGateways() { + h.hostsLock.Lock() + defer h.hostsLock.Unlock() + h.availableGatewayHosts = make(map[string]struct{}) + h.drainingGatewayHosts = make(map[string]struct{}) + h.unreachableGatewayHosts = make(map[string]struct{}) + for _, host := range h.gatewayHosts { + h.availableGatewayHosts[host] = struct{}{} + } +} diff --git a/connect/handler.go b/connect/handler.go new file mode 100644 index 00000000..b4230041 --- /dev/null +++ b/connect/handler.go @@ -0,0 +1,854 @@ +package connect + +import ( + "context" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "github.com/coder/websocket" + "github.com/inngest/inngest/pkg/connect/types" + "github.com/inngest/inngest/pkg/connect/wsproto" + "github.com/inngest/inngest/pkg/enums" + "github.com/inngest/inngest/pkg/execution/state" + "github.com/inngest/inngest/pkg/publicerr" + "github.com/inngest/inngest/pkg/sdk" + "github.com/inngest/inngest/pkg/syscode" + connectproto "github.com/inngest/inngest/proto/gen/connect/v1" + sdkerrors "github.com/inngest/inngestgo/errors" + "github.com/inngest/inngestgo/internal/sdkrequest" + "github.com/oklog/ulid/v2" + "github.com/pbnjay/memory" + "golang.org/x/exp/slog" + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/proto" + "io" + "net" + "os" + "runtime" + "strings" + "sync" + "time" +) + +const ( + WorkerHeartbeatInterval = 10 * time.Second +) + +type FunctionInvoker interface { + InvokeFunction(ctx context.Context, slug string, stepId *string, request sdkrequest.Request) (any, []state.GeneratorOpcode, error) +} + +type Opts struct { + AppName string + Env *string + + Functions []sdk.SDKFunction + Capabilities sdk.Capabilities + + HashedSigningKey []byte + HashedSigningKeyFallback []byte + + WorkerConcurrency int + + APIBaseUrl string + IsDev bool + DevServerUrl string + ConnectUrls []string + + InstanceId *string + BuildId *string + + Platform *string + SDKVersion string + SDKLanguage string +} + +type connectHandler struct { + opts Opts + + invoker FunctionInvoker + + logger *slog.Logger + + connectionId ulid.ULID + + messageBuffer []*connectproto.ConnectMessage + messageBufferLock sync.Mutex + + hostsManager *hostsManager + + workerPool *workerPool +} + +// authContext is wrapper for information related to authentication +type authContext struct { + hashedSigningKey []byte + fallback bool +} + +func (h *connectHandler) connectURLs() []string { + if h.opts.IsDev { + return []string{fmt.Sprintf("%s/connect", strings.Replace(h.opts.DevServerUrl, "http", "ws", 1))} + } + + if len(h.opts.ConnectUrls) > 0 { + return h.opts.ConnectUrls + } + + return nil +} + +func (h *connectHandler) instanceId() string { + if h.opts.InstanceId != nil { + return *h.opts.InstanceId + } + + hostname, _ := os.Hostname() + if hostname != "" { + return hostname + } + + // TODO Is there any stable identifier that can be used as a fallback? + return "" +} + +func Connect(ctx context.Context, opts Opts, invoker FunctionInvoker, logger *slog.Logger) error { + ch := &connectHandler{ + logger: logger, + invoker: invoker, + } + wp := NewWorkerPool(opts.WorkerConcurrency, ch.processExecutorRequest) + ch.workerPool = wp + + defer func() { + // TODO Push remaining messages to another destination for processing? + }() + + err := ch.Connect(ctx) + if err != nil { + return fmt.Errorf("could not establish connection: %w", err) + } + + return nil +} + +func (h *connectHandler) Connect(ctx context.Context) error { + signingKey := h.opts.HashedSigningKey + if len(signingKey) == 0 { + return fmt.Errorf("hashed signing key is required") + } + + auth := authContext{hashedSigningKey: signingKey} + + numCpuCores := runtime.NumCPU() + totalMem := memory.TotalMemory() + + marshaledFns, err := json.Marshal(h.opts.Functions) + if err != nil { + return fmt.Errorf("failed to serialize connect config: %w", err) + } + + marshaledCapabilities, err := json.Marshal(h.opts.Capabilities) + if err != nil { + return fmt.Errorf("failed to serialize connect config: %w", err) + } + + hosts := h.connectURLs() + if len(hosts) == 0 { + return fmt.Errorf("no connect URLs provided") + } + + h.hostsManager = newHostsManager(hosts) + + eg := errgroup.Group{} + + notifyConnectDoneChan := make(chan connectReport) + notifyConnectedChan := make(chan struct{}) + initiateConnectionChan := make(chan struct{}) + + var attempts int + eg.Go(func() error { + for { + select { + case <-ctx.Done(): + return nil + case <-notifyConnectedChan: + attempts = 0 + continue + case msg := <-notifyConnectDoneChan: + h.logger.Error("connect failed", "err", err, "reconnect", msg.reconnect) + + if !msg.reconnect { + return err + } + + if msg.err != nil { + closeErr := websocket.CloseError{} + if errors.As(err, &closeErr) { + switch closeErr.Reason { + // If auth failed, retry with fallback key + case syscode.CodeConnectAuthFailed: + if auth.fallback { + return fmt.Errorf("failed to authenticate with fallback key, exiting") + } + + signingKeyFallback := h.opts.HashedSigningKeyFallback + if len(signingKeyFallback) == 0 { + return fmt.Errorf("fallback signing key is required") + } + + auth = authContext{hashedSigningKey: signingKeyFallback, fallback: true} + + initiateConnectionChan <- struct{}{} + + continue + + // Retry on the following error codes + case syscode.CodeConnectGatewayClosing, syscode.CodeConnectInternal, syscode.CodeConnectWorkerHelloTimeout: + initiateConnectionChan <- struct{}{} + + continue + + default: + // If we received a reason that's non-retriable, stop here. + return fmt.Errorf("connect failed with error code %q", closeErr.Reason) + } + } + } + + initiateConnectionChan <- struct{}{} + case <-initiateConnectionChan: + } + + if attempts == 5 { + return fmt.Errorf("could not establish connection after 5 attempts") + } + + attempts++ + + h.connect(ctx, false, connectionEstablishData{ + hashedSigningKey: auth.hashedSigningKey, + numCpuCores: int32(numCpuCores), + totalMem: int64(totalMem), + marshaledFns: marshaledFns, + marshaledCapabilities: marshaledCapabilities, + }, notifyConnectedChan, notifyConnectDoneChan) + } + }) + + initiateConnectionChan <- struct{}{} + + if err := eg.Wait(); err != nil { + return fmt.Errorf("could not establish connection: %w", err) + } + + // Send out buffered messages, using new connection if necessary! + h.messageBufferLock.Lock() + defer h.messageBufferLock.Unlock() + if len(h.messageBuffer) > 0 { + // Send buffered messages via a working connection + err = h.withTemporaryConnection(connectionEstablishData{ + hashedSigningKey: auth.hashedSigningKey, + numCpuCores: int32(numCpuCores), + totalMem: int64(totalMem), + marshaledFns: marshaledFns, + marshaledCapabilities: marshaledCapabilities, + }, func(ws *websocket.Conn) error { + // Send buffered messages + err := h.sendBufferedMessages(ws) + if err != nil { + return fmt.Errorf("could not send buffered messages: %w", err) + } + + return nil + }) + if err != nil { + h.logger.Error("could not establish connection for sending buffered messages", "err", err) + } + + // TODO Push remaining messages to another destination for processing? + } + + return nil +} + +type connectionEstablishData struct { + hashedSigningKey []byte + numCpuCores int32 + totalMem int64 + marshaledFns []byte + marshaledCapabilities []byte + manualReadinessAck bool +} + +type preparedConnection struct { + ws *websocket.Conn + gatewayHost string +} + +func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGateways bool, data connectionEstablishData) (preparedConnection, bool, error) { + connectTimeout, cancelConnectTimeout := context.WithTimeout(ctx, 10*time.Second) + defer cancelConnectTimeout() + + gatewayHost := h.hostsManager.pickAvailableGateway() + if gatewayHost == "" { + h.hostsManager.resetGateways() + + return preparedConnection{}, allowResettingGateways, fmt.Errorf("no available gateway hosts") + } + + // Establish WebSocket connection to one of the gateways + ws, _, err := websocket.Dial(connectTimeout, gatewayHost, &websocket.DialOptions{ + Subprotocols: []string{ + types.GatewaySubProtocol, + }, + }) + if err != nil { + h.hostsManager.markUnreachableGateway(gatewayHost) + return preparedConnection{}, false, fmt.Errorf("could not connect to gateway: %w", err) + } + + // Connection ID is unique per connection, reconnections should get a new ID + h.connectionId = ulid.MustNew(ulid.Now(), rand.Reader) + + h.logger.Debug("websocket connection established") + + // Wait for gateway hello message + { + initialMessageTimeout, cancelInitialTimeout := context.WithTimeout(ctx, 5*time.Second) + defer cancelInitialTimeout() + var helloMessage connectproto.ConnectMessage + err = wsproto.Read(initialMessageTimeout, ws, &helloMessage) + if err != nil { + h.hostsManager.markUnreachableGateway(gatewayHost) + return preparedConnection{}, true, fmt.Errorf("did not receive gateway hello message: %w", err) + } + + if helloMessage.Kind != connectproto.GatewayMessageType_GATEWAY_HELLO { + h.hostsManager.markUnreachableGateway(gatewayHost) + return preparedConnection{}, true, fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind) + } + + h.logger.Debug("received gateway hello message") + } + + // Send connect message + { + + apiOrigin := h.opts.APIBaseUrl + if h.opts.IsDev { + apiOrigin = h.opts.DevServerUrl + } + + data, err := proto.Marshal(&connectproto.WorkerConnectRequestData{ + SessionId: &connectproto.SessionIdentifier{ + BuildId: h.opts.BuildId, + InstanceId: h.instanceId(), + ConnectionId: h.connectionId.String(), + }, + AuthData: &connectproto.AuthData{ + HashedSigningKey: data.hashedSigningKey, + }, + AppName: h.opts.AppName, + Config: &connectproto.ConfigDetails{ + Capabilities: data.marshaledCapabilities, + Functions: data.marshaledFns, + ApiOrigin: apiOrigin, + }, + SystemAttributes: &connectproto.SystemAttributes{ + CpuCores: data.numCpuCores, + MemBytes: data.totalMem, + Os: runtime.GOOS, + }, + Environment: h.opts.Env, + Platform: h.opts.Platform, + SdkVersion: h.opts.SDKVersion, + SdkLanguage: h.opts.SDKLanguage, + WorkerManualReadinessAck: data.manualReadinessAck, + }) + if err != nil { + return preparedConnection{}, false, fmt.Errorf("could not serialize sdk connect message: %w", err) + } + + err = wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_CONNECT, + Payload: data, + }) + if err != nil { + return preparedConnection{}, true, fmt.Errorf("could not send initial message") + } + } + + // Wait for gateway ready message + { + connectionReadyTimeout, cancelConnectionReadyTimeout := context.WithTimeout(ctx, 20*time.Second) + defer cancelConnectionReadyTimeout() + var connectionReadyMsg connectproto.ConnectMessage + err = wsproto.Read(connectionReadyTimeout, ws, &connectionReadyMsg) + if err != nil { + return preparedConnection{}, true, fmt.Errorf("did not receive gateway connection ready message: %w", err) + } + + if connectionReadyMsg.Kind != connectproto.GatewayMessageType_GATEWAY_CONNECTION_READY { + return preparedConnection{}, true, fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind) + } + + h.logger.Debug("received gateway connection ready message") + } + + return preparedConnection{ws, gatewayHost}, false, nil +} + +func (h *connectHandler) sendBufferedMessages(ws *websocket.Conn) error { + processed := 0 + for _, msg := range h.messageBuffer { + // always send the message, even if the context is cancelled + err := wsproto.Write(context.Background(), ws, msg) + if err != nil { + // Only send buffered messages once + h.messageBuffer = h.messageBuffer[processed:] + + h.logger.Error("failed to send buffered message", "err", err) + return fmt.Errorf("could not send buffered message: %w", err) + } + + h.logger.Debug("sent buffered message", "msg", msg) + processed++ + } + h.messageBuffer = nil + return nil +} + +var errGatewayDraining = errors.New("gateway is draining") + +func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) (reconnect bool, err error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + defer func() { + // TODO Do we need to include a reason here? If we only use this for unexpected disconnects, probably not + _ = ws.CloseNow() + }() + + go func() { + <-ctx.Done() + _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) + }() + + // Send buffered but unsent messages if connection was re-established + if len(h.messageBuffer) > 0 { + h.logger.Debug("sending buffered messages", "count", len(h.messageBuffer)) + err = h.sendBufferedMessages(ws) + if err != nil { + return true, fmt.Errorf("could not send buffered messages: %w", err) + } + } + + go func() { + heartbeatTicker := time.NewTicker(WorkerHeartbeatInterval) + defer heartbeatTicker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-heartbeatTicker.C: + err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_HEARTBEAT, + }) + if err != nil { + h.logger.Error("failed to send worker heartbeat", "err", err) + } + } + + } + }() + + eg := errgroup.Group{} + eg.Go(func() error { + for { + var msg connectproto.ConnectMessage + err = wsproto.Read(context.Background(), ws, &msg) + if err != nil { + h.logger.Error("failed to read message", "err", err) + + // The connection may still be active, but for some reason we couldn't read the message + return err + } + + h.logger.Debug("received gateway request", "msg", &msg) + + switch msg.Kind { + case connectproto.GatewayMessageType_GATEWAY_CLOSING: + // Stop the read loop: We will not receive any further messages and should establish a new connection + // We can still use the old connection to send replies to the gateway + return errGatewayDraining + case connectproto.GatewayMessageType_GATEWAY_EXECUTOR_REQUEST: + // Handle invoke in a non-blocking way to allow for other messages to be processed + h.workerPool.Add(workerPoolMsg{ + msg: &msg, + ws: ws, + }) + default: + h.logger.Error("got unknown gateway request", "err", err) + continue + } + } + }) + + h.logger.Debug("waiting for read loop to end") + + // If read loop ends, this can be for two reasons + // - Connection loss (io.EOF), read loop terminated intentionally (CloseError), other error (unexpected) + // - Worker shutdown, parent context got cancelled + if err := eg.Wait(); err != nil && ctx.Err() == nil { + if errors.Is(err, errGatewayDraining) { + h.hostsManager.markDrainingGateway(gatewayHost) + + // Gateway is draining and will not accept new connections. + // We must reconnect to a different gateway, only then can we close the old connection. + waitUntilConnected, doneWaiting := context.WithCancel(context.Background()) + defer doneWaiting() + + // Intercept connected signal and pass it to the main goroutine + notifyConnectedInterceptChan := make(chan struct{}) + go func() { + <-notifyConnectedChan + notifyConnectedInterceptChan <- struct{}{} + doneWaiting() + }() + + // Establish new connection and pass close reports back to the main goroutine + h.connect(ctx, true, data, notifyConnectedInterceptChan, notifyConnectDoneChan) + + // Wait until the new connection is established before closing the old one + <-waitUntilConnected.Done() + + // By returning, we will close the old connection + return false, errGatewayDraining + } + + h.logger.Debug("read loop ended with error", "err", err) + + // In case the gateway intentionally closed the connection, we'll receive a close error + cerr := websocket.CloseError{} + if errors.As(err, &cerr) { + h.logger.Error("connection closed with reason", "reason", cerr.Reason) + + // Reconnect! + return true, fmt.Errorf("connection closed with reason %q: %w", cerr.Reason, cerr) + } + + // connection closed without reason + if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { + h.logger.Error("failed to read message from gateway, lost connection unexpectedly", "err", err) + return true, fmt.Errorf("connection closed unexpectedly: %w", cerr) + } + + // If this is not a worker shutdown, we should reconnect + return true, fmt.Errorf("connection closed unexpectedly: %w", ctx.Err()) + } + + // Perform graceful shutdown routine (context was cancelled) + + // Signal gateway that we won't process additional messages! + { + h.logger.Debug("sending worker pause message") + err := wsproto.Write(context.Background(), ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_PAUSE, + }) + if err != nil { + // We should not exit here, as we're already in the shutdown routine + h.logger.Error("failed to serialize worker pause msg", "err", err) + } + } + + h.logger.Debug("waiting for in-progress requests to finish") + + // Wait until all in-progress requests are completed + h.workerPool.Wait() + + // Attempt to shut down connection if not already done + _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) + + return false, nil +} + +type connectReport struct { + reconnect bool + err error +} + +func (h *connectHandler) connect(ctx context.Context, allowResettingGateways bool, data connectionEstablishData, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) { + preparedConn, reconnect, err := h.prepareConnection(ctx, allowResettingGateways, data) + if err != nil { + notifyConnectDoneChan <- connectReport{ + reconnect: reconnect, + err: fmt.Errorf("could not establish connection: %w", err), + } + return + } + + notifyConnectedChan <- struct{}{} + + reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost, notifyConnectedChan, notifyConnectDoneChan) + if err != nil { + if errors.Is(err, errGatewayDraining) { + // if the gateway is draining, the original connection was closed, and we already reconnected inside handleConnection + return + } + + notifyConnectDoneChan <- connectReport{ + reconnect: reconnect, + err: fmt.Errorf("could not handle connection: %w", err), + } + return + } + + notifyConnectDoneChan <- connectReport{ + reconnect: reconnect, + err: nil, + } +} + +func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, handler func(ws *websocket.Conn) error) error { + // Prevent this connection from receiving work + data.manualReadinessAck = true + + maxAttempts := 4 + + var conn *websocket.Conn + var attempts int + for { + if attempts == maxAttempts { + return fmt.Errorf("could not establish connection after %d attempts", maxAttempts) + } + + ws, _, err := h.prepareConnection(context.Background(), true, data) + if err != nil { + attempts++ + continue + } + + conn = ws.ws + break + } + + defer func() { + _ = conn.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) + }() + + err := handler(conn) + if err != nil { + return err + } + + return nil +} + +func (h *connectHandler) processExecutorRequest(msg workerPoolMsg) { + defer h.workerPool.Done() + + // Always make sure the invoke finishes properly + processCtx := context.Background() + + err := h.handleInvokeMessage(processCtx, msg.ws, msg.msg) + + // When we encounter an error, we cannot retry the connection from inside the goroutine. + // If we're dealing with connection loss, the next read loop will fail with the same error + // and handle the reconnection. + if err != nil { + cerr := websocket.CloseError{} + if errors.As(err, &cerr) { + h.logger.Error("gateway connection closed with reason", "reason", cerr.Reason) + return + } + + if errors.Is(err, io.EOF) { + h.logger.Error("gateway connection closed unexpectedly", "err", err) + return + } + + // TODO If error is not connection-related, should we retry? Send the buffered message? + } +} + +func (h *connectHandler) handleInvokeMessage(ctx context.Context, ws *websocket.Conn, msg *connectproto.ConnectMessage) error { + resp, err := h.connectInvoke(ctx, ws, msg) + if err != nil { + h.logger.Error("failed to handle sdk request", "err", err) + // TODO Should we drop the connection? Continue receiving messages? + return fmt.Errorf("could not handle sdk request: %w", err) + } + + data, err := proto.Marshal(resp) + if err != nil { + h.logger.Error("failed to serialize sdk response", "err", err) + // TODO This should never happen; Signal that we should retry + return fmt.Errorf("could not serialize sdk response: %w", err) + } + + responseMessage := &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_REPLY, + Payload: data, + } + + err = wsproto.Write(ctx, ws, responseMessage) + if err != nil { + h.logger.Error("failed to send sdk response", "err", err) + + // Buffer message to retry + h.messageBufferLock.Lock() + h.messageBuffer = append(h.messageBuffer, responseMessage) + h.messageBufferLock.Unlock() + + return fmt.Errorf("could not send sdk response: %w", err) + } + + return nil +} + +// connectInvoke is the counterpart to invoke for connect +func (h *connectHandler) connectInvoke(ctx context.Context, ws *websocket.Conn, msg *connectproto.ConnectMessage) (*connectproto.SDKResponse, error) { + body := connectproto.GatewayExecutorRequestData{} + if err := proto.Unmarshal(msg.Payload, &body); err != nil { + // TODO Should we send this back to the gateway? + h.logger.Error("error decoding gateway request data", "error", err) + return nil, fmt.Errorf("invalid gateway message data: %w", err) + } + + // Note: This still uses JSON + // TODO Replace with Protobuf + var request sdkrequest.Request + if err := json.Unmarshal(body.RequestPayload, &request); err != nil { + // TODO Should we send this back to the gateway? Previously this was a status code 400 public error with "malformed input" + h.logger.Error("error decoding sdk request", "error", err) + return nil, fmt.Errorf("invalid SDK request payload: %w", err) + } + + ackPayload, err := proto.Marshal(&connectproto.WorkerRequestAckData{ + RequestId: body.RequestId, + AppId: body.AppId, + FunctionSlug: body.FunctionSlug, + StepId: body.StepId, + }) + if err != nil { + h.logger.Error("error marshaling request ack", "error", err) + return nil, publicerr.Error{ + Message: "malformed input", + Status: 400, + } + } + + // Ack message + // If we're shutting down (context is canceled) we will not ack, which is desired! + if err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_REQUEST_ACK, + Payload: ackPayload, + }); err != nil { + h.logger.Error("error sending request ack", "error", err) + return nil, publicerr.Error{ + Message: "failed to ack worker request", + Status: 400, + } + } + + // TODO Should we wait for a gateway response before starting to process? What if the gateway fails acking and we start too early? + // This should not happen but could lead to double processing of the same message + + if request.UseAPI { + // TODO: implement this + // retrieve data from API + // request.Steps = + // request.Events = + _ = 0 // no-op to avoid linter error + } + + var stepId *string + if body.StepId != nil && *body.StepId != "step" { + stepId = body.StepId + } + + // Invoke function, always complete regardless of + resp, ops, err := h.invoker.InvokeFunction(context.Background(), body.FunctionSlug, stepId, request) + + // NOTE: When triggering step errors, we should have an OpcodeStepError + // within ops alongside an error. We can safely ignore that error, as it's + // only used for checking whether the step used a NoRetryError or RetryAtError + // + // For that reason, we check those values first. + noRetry := sdkerrors.IsNoRetryError(err) + retryAt := sdkerrors.GetRetryAtTime(err) + if len(ops) == 1 && ops[0].Op == enums.OpcodeStepError { + // Now we've handled error types we can ignore step + // errors safely. + err = nil + } + + // Now that we've handled the OpcodeStepError, if we *still* ahve + // a StepError kind returned from a function we must have an unhandled + // step error. This is a NonRetryableError, as the most likely code is: + // + // _, err := step.Run(ctx, func() (any, error) { return fmt.Errorf("") }) + // if err != nil { + // return err + // } + if sdkerrors.IsStepError(err) { + err = fmt.Errorf("Unhandled step error: %s", err) + noRetry = true + } + + // These may be added even for 2xx codes with step errors. + var retryAfterVal *string + if retryAt != nil { + formatted := retryAt.Format(time.RFC3339) + retryAfterVal = &formatted + } + + if err != nil { + h.logger.Error("error calling function", "error", err) + return &connectproto.SDKResponse{ + RequestId: body.RequestId, + Status: connectproto.SDKResponseStatus_ERROR, + Body: []byte(fmt.Sprintf("error calling function: %s", err.Error())), + NoRetry: noRetry, + RetryAfter: retryAfterVal, + }, nil + } + + if len(ops) > 0 { + // Note: This still uses JSON + // TODO Replace with Protobuf + serializedOps, err := json.Marshal(ops) + if err != nil { + return nil, fmt.Errorf("could not serialize ops: %w", err) + } + + // Return the function opcode returned here so that we can re-invoke this + // function and manage state appropriately. Any opcode here takes precedence + // over function return values as the function has not yet finished. + return &connectproto.SDKResponse{ + RequestId: body.RequestId, + Status: connectproto.SDKResponseStatus_NOT_COMPLETED, + Body: serializedOps, + NoRetry: noRetry, + RetryAfter: retryAfterVal, + }, nil + } + + // Note: This still uses JSON + // TODO Replace with Protobuf + serializedResp, err := json.Marshal(resp) + if err != nil { + return nil, fmt.Errorf("could not serialize resp: %w", err) + } + + // Return the function response. + return &connectproto.SDKResponse{ + RequestId: body.RequestId, + Status: connectproto.SDKResponseStatus_DONE, + Body: serializedResp, + NoRetry: noRetry, + RetryAfter: retryAfterVal, + }, nil +} diff --git a/connect/worker_pool.go b/connect/worker_pool.go new file mode 100644 index 00000000..0ac40514 --- /dev/null +++ b/connect/worker_pool.go @@ -0,0 +1,62 @@ +package connect + +import ( + "context" + "github.com/coder/websocket" + connectproto "github.com/inngest/inngest/proto/gen/connect/v1" + "sync" +) + +type workerPoolMsg struct { + msg *connectproto.ConnectMessage + ws *websocket.Conn +} + +type workerPool struct { + concurrency int + handler func(msg workerPoolMsg) + inProgress sync.WaitGroup + workerPoolMsgs chan workerPoolMsg +} + +func NewWorkerPool(concurrency int, handler func(msg workerPoolMsg)) *workerPool { + wp := &workerPool{ + // Should this use the same buffer size as the worker pool? + workerPoolMsgs: make(chan workerPoolMsg, concurrency), + concurrency: concurrency, + inProgress: sync.WaitGroup{}, + handler: handler, + } + + return wp +} + +func (w *workerPool) Start(ctx context.Context) { + for i := 0; i < w.concurrency; i++ { + go w.workerPool(ctx) + } +} + +func (w *workerPool) workerPool(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case msg := <-w.workerPoolMsgs: + w.handler(msg) + } + } +} + +func (w *workerPool) Add(msg workerPoolMsg) { + w.inProgress.Add(1) + w.workerPoolMsgs <- msg +} + +func (w *workerPool) Wait() { + w.inProgress.Wait() +} + +func (w *workerPool) Done() { + w.inProgress.Done() +} diff --git a/handler.go b/handler.go index 53f64cb3..484407fa 100644 --- a/handler.go +++ b/handler.go @@ -624,7 +624,6 @@ func (h *handler) outOfBandSync(w http.ResponseWriter, r *http.Request) error { Platform: platform(), }, Capabilities: capabilities, - UseConnect: h.useConnect, } fns, err := createFunctionConfigs(h.appName, h.funcs, *h.url(r), false) From 9fc445f30df812ea90b602baa9ce00dc68018c65 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 15:48:15 +0100 Subject: [PATCH 06/16] Split out connection management and function invocation --- connect/connection.go | 370 +++++++++++++++++++++++++ connect/handler.go | 615 +++--------------------------------------- connect/invoke.go | 196 ++++++++++++++ handler.go | 2 - 4 files changed, 608 insertions(+), 575 deletions(-) create mode 100644 connect/connection.go create mode 100644 connect/invoke.go diff --git a/connect/connection.go b/connect/connection.go new file mode 100644 index 00000000..87907d35 --- /dev/null +++ b/connect/connection.go @@ -0,0 +1,370 @@ +package connect + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "github.com/coder/websocket" + "github.com/inngest/inngest/pkg/connect/types" + "github.com/inngest/inngest/pkg/connect/wsproto" + connectproto "github.com/inngest/inngest/proto/gen/connect/v1" + "github.com/oklog/ulid/v2" + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/proto" + "io" + "net" + "runtime" + "time" +) + +type connectReport struct { + reconnect bool + err error +} + +func (h *connectHandler) connect(ctx context.Context, allowResettingGateways bool, data connectionEstablishData, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) { + preparedConn, reconnect, err := h.prepareConnection(ctx, allowResettingGateways, data) + if err != nil { + notifyConnectDoneChan <- connectReport{ + reconnect: reconnect, + err: fmt.Errorf("could not establish connection: %w", err), + } + return + } + + notifyConnectedChan <- struct{}{} + + reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost, notifyConnectedChan, notifyConnectDoneChan) + if err != nil { + if errors.Is(err, errGatewayDraining) { + // if the gateway is draining, the original connection was closed, and we already reconnected inside handleConnection + return + } + + notifyConnectDoneChan <- connectReport{ + reconnect: reconnect, + err: fmt.Errorf("could not handle connection: %w", err), + } + return + } + + notifyConnectDoneChan <- connectReport{ + reconnect: reconnect, + err: nil, + } +} + +type connectionEstablishData struct { + hashedSigningKey []byte + numCpuCores int32 + totalMem int64 + marshaledFns []byte + marshaledCapabilities []byte + manualReadinessAck bool +} + +type preparedConnection struct { + ws *websocket.Conn + gatewayHost string +} + +func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGateways bool, data connectionEstablishData) (preparedConnection, bool, error) { + connectTimeout, cancelConnectTimeout := context.WithTimeout(ctx, 10*time.Second) + defer cancelConnectTimeout() + + gatewayHost := h.hostsManager.pickAvailableGateway() + if gatewayHost == "" { + h.hostsManager.resetGateways() + + return preparedConnection{}, allowResettingGateways, fmt.Errorf("no available gateway hosts") + } + + // Establish WebSocket connection to one of the gateways + ws, _, err := websocket.Dial(connectTimeout, gatewayHost, &websocket.DialOptions{ + Subprotocols: []string{ + types.GatewaySubProtocol, + }, + }) + if err != nil { + h.hostsManager.markUnreachableGateway(gatewayHost) + return preparedConnection{}, false, fmt.Errorf("could not connect to gateway: %w", err) + } + + // Connection ID is unique per connection, reconnections should get a new ID + h.connectionId = ulid.MustNew(ulid.Now(), rand.Reader) + + h.logger.Debug("websocket connection established") + + // Wait for gateway hello message + { + initialMessageTimeout, cancelInitialTimeout := context.WithTimeout(ctx, 5*time.Second) + defer cancelInitialTimeout() + var helloMessage connectproto.ConnectMessage + err = wsproto.Read(initialMessageTimeout, ws, &helloMessage) + if err != nil { + h.hostsManager.markUnreachableGateway(gatewayHost) + return preparedConnection{}, true, fmt.Errorf("did not receive gateway hello message: %w", err) + } + + if helloMessage.Kind != connectproto.GatewayMessageType_GATEWAY_HELLO { + h.hostsManager.markUnreachableGateway(gatewayHost) + return preparedConnection{}, true, fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind) + } + + h.logger.Debug("received gateway hello message") + } + + // Send connect message + { + + apiOrigin := h.opts.APIBaseUrl + if h.opts.IsDev { + apiOrigin = h.opts.DevServerUrl + } + + data, err := proto.Marshal(&connectproto.WorkerConnectRequestData{ + SessionId: &connectproto.SessionIdentifier{ + BuildId: h.opts.BuildId, + InstanceId: h.instanceId(), + ConnectionId: h.connectionId.String(), + }, + AuthData: &connectproto.AuthData{ + HashedSigningKey: data.hashedSigningKey, + }, + AppName: h.opts.AppName, + Config: &connectproto.ConfigDetails{ + Capabilities: data.marshaledCapabilities, + Functions: data.marshaledFns, + ApiOrigin: apiOrigin, + }, + SystemAttributes: &connectproto.SystemAttributes{ + CpuCores: data.numCpuCores, + MemBytes: data.totalMem, + Os: runtime.GOOS, + }, + Environment: h.opts.Env, + Platform: h.opts.Platform, + SdkVersion: h.opts.SDKVersion, + SdkLanguage: h.opts.SDKLanguage, + WorkerManualReadinessAck: data.manualReadinessAck, + }) + if err != nil { + return preparedConnection{}, false, fmt.Errorf("could not serialize sdk connect message: %w", err) + } + + err = wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_CONNECT, + Payload: data, + }) + if err != nil { + return preparedConnection{}, true, fmt.Errorf("could not send initial message") + } + } + + // Wait for gateway ready message + { + connectionReadyTimeout, cancelConnectionReadyTimeout := context.WithTimeout(ctx, 20*time.Second) + defer cancelConnectionReadyTimeout() + var connectionReadyMsg connectproto.ConnectMessage + err = wsproto.Read(connectionReadyTimeout, ws, &connectionReadyMsg) + if err != nil { + return preparedConnection{}, true, fmt.Errorf("did not receive gateway connection ready message: %w", err) + } + + if connectionReadyMsg.Kind != connectproto.GatewayMessageType_GATEWAY_CONNECTION_READY { + return preparedConnection{}, true, fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind) + } + + h.logger.Debug("received gateway connection ready message") + } + + return preparedConnection{ws, gatewayHost}, false, nil +} + +func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) (reconnect bool, err error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + defer func() { + // TODO Do we need to include a reason here? If we only use this for unexpected disconnects, probably not + _ = ws.CloseNow() + }() + + go func() { + <-ctx.Done() + _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) + }() + + // Send buffered but unsent messages if connection was re-established + if len(h.messageBuffer) > 0 { + h.logger.Debug("sending buffered messages", "count", len(h.messageBuffer)) + err = h.sendBufferedMessages(ws) + if err != nil { + return true, fmt.Errorf("could not send buffered messages: %w", err) + } + } + + go func() { + heartbeatTicker := time.NewTicker(WorkerHeartbeatInterval) + defer heartbeatTicker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-heartbeatTicker.C: + err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_HEARTBEAT, + }) + if err != nil { + h.logger.Error("failed to send worker heartbeat", "err", err) + } + } + + } + }() + + eg := errgroup.Group{} + eg.Go(func() error { + for { + var msg connectproto.ConnectMessage + err = wsproto.Read(context.Background(), ws, &msg) + if err != nil { + h.logger.Error("failed to read message", "err", err) + + // The connection may still be active, but for some reason we couldn't read the message + return err + } + + h.logger.Debug("received gateway request", "msg", &msg) + + switch msg.Kind { + case connectproto.GatewayMessageType_GATEWAY_CLOSING: + // Stop the read loop: We will not receive any further messages and should establish a new connection + // We can still use the old connection to send replies to the gateway + return errGatewayDraining + case connectproto.GatewayMessageType_GATEWAY_EXECUTOR_REQUEST: + // Handle invoke in a non-blocking way to allow for other messages to be processed + h.workerPool.Add(workerPoolMsg{ + msg: &msg, + ws: ws, + }) + default: + h.logger.Error("got unknown gateway request", "err", err) + continue + } + } + }) + + h.logger.Debug("waiting for read loop to end") + + // If read loop ends, this can be for two reasons + // - Connection loss (io.EOF), read loop terminated intentionally (CloseError), other error (unexpected) + // - Worker shutdown, parent context got cancelled + if err := eg.Wait(); err != nil && ctx.Err() == nil { + if errors.Is(err, errGatewayDraining) { + h.hostsManager.markDrainingGateway(gatewayHost) + + // Gateway is draining and will not accept new connections. + // We must reconnect to a different gateway, only then can we close the old connection. + waitUntilConnected, doneWaiting := context.WithCancel(context.Background()) + defer doneWaiting() + + // Intercept connected signal and pass it to the main goroutine + notifyConnectedInterceptChan := make(chan struct{}) + go func() { + <-notifyConnectedChan + notifyConnectedInterceptChan <- struct{}{} + doneWaiting() + }() + + // Establish new connection and pass close reports back to the main goroutine + h.connect(ctx, true, data, notifyConnectedInterceptChan, notifyConnectDoneChan) + + // Wait until the new connection is established before closing the old one + <-waitUntilConnected.Done() + + // By returning, we will close the old connection + return false, errGatewayDraining + } + + h.logger.Debug("read loop ended with error", "err", err) + + // In case the gateway intentionally closed the connection, we'll receive a close error + cerr := websocket.CloseError{} + if errors.As(err, &cerr) { + h.logger.Error("connection closed with reason", "reason", cerr.Reason) + + // Reconnect! + return true, fmt.Errorf("connection closed with reason %q: %w", cerr.Reason, cerr) + } + + // connection closed without reason + if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { + h.logger.Error("failed to read message from gateway, lost connection unexpectedly", "err", err) + return true, fmt.Errorf("connection closed unexpectedly: %w", cerr) + } + + // If this is not a worker shutdown, we should reconnect + return true, fmt.Errorf("connection closed unexpectedly: %w", ctx.Err()) + } + + // Perform graceful shutdown routine (context was cancelled) + + // Signal gateway that we won't process additional messages! + { + h.logger.Debug("sending worker pause message") + err := wsproto.Write(context.Background(), ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_PAUSE, + }) + if err != nil { + // We should not exit here, as we're already in the shutdown routine + h.logger.Error("failed to serialize worker pause msg", "err", err) + } + } + + h.logger.Debug("waiting for in-progress requests to finish") + + // Wait until all in-progress requests are completed + h.workerPool.Wait() + + // Attempt to shut down connection if not already done + _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) + + return false, nil +} + +func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, handler func(ws *websocket.Conn) error) error { + // Prevent this connection from receiving work + data.manualReadinessAck = true + + maxAttempts := 4 + + var conn *websocket.Conn + var attempts int + for { + if attempts == maxAttempts { + return fmt.Errorf("could not establish connection after %d attempts", maxAttempts) + } + + ws, _, err := h.prepareConnection(context.Background(), true, data) + if err != nil { + attempts++ + continue + } + + conn = ws.ws + break + } + + defer func() { + _ = conn.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) + }() + + err := handler(conn) + if err != nil { + return err + } + + return nil +} diff --git a/connect/handler.go b/connect/handler.go index b4230041..519aabcc 100644 --- a/connect/handler.go +++ b/connect/handler.go @@ -2,28 +2,21 @@ package connect import ( "context" - "crypto/rand" "encoding/json" "errors" "fmt" "github.com/coder/websocket" - "github.com/inngest/inngest/pkg/connect/types" "github.com/inngest/inngest/pkg/connect/wsproto" - "github.com/inngest/inngest/pkg/enums" "github.com/inngest/inngest/pkg/execution/state" - "github.com/inngest/inngest/pkg/publicerr" "github.com/inngest/inngest/pkg/sdk" "github.com/inngest/inngest/pkg/syscode" connectproto "github.com/inngest/inngest/proto/gen/connect/v1" - sdkerrors "github.com/inngest/inngestgo/errors" "github.com/inngest/inngestgo/internal/sdkrequest" "github.com/oklog/ulid/v2" "github.com/pbnjay/memory" "golang.org/x/exp/slog" "golang.org/x/sync/errgroup" - "google.golang.org/protobuf/proto" "io" - "net" "os" "runtime" "strings" @@ -35,6 +28,26 @@ const ( WorkerHeartbeatInterval = 10 * time.Second ) +func Connect(ctx context.Context, opts Opts, invoker FunctionInvoker, logger *slog.Logger) error { + ch := &connectHandler{ + logger: logger, + invoker: invoker, + } + wp := NewWorkerPool(opts.WorkerConcurrency, ch.processExecutorRequest) + ch.workerPool = wp + + defer func() { + // TODO Push remaining messages to another destination for processing? + }() + + err := ch.Connect(ctx) + if err != nil { + return fmt.Errorf("could not establish connection: %w", err) + } + + return nil +} + type FunctionInvoker interface { InvokeFunction(ctx context.Context, slug string, stepId *string, request sdkrequest.Request) (any, []state.GeneratorOpcode, error) } @@ -87,52 +100,6 @@ type authContext struct { fallback bool } -func (h *connectHandler) connectURLs() []string { - if h.opts.IsDev { - return []string{fmt.Sprintf("%s/connect", strings.Replace(h.opts.DevServerUrl, "http", "ws", 1))} - } - - if len(h.opts.ConnectUrls) > 0 { - return h.opts.ConnectUrls - } - - return nil -} - -func (h *connectHandler) instanceId() string { - if h.opts.InstanceId != nil { - return *h.opts.InstanceId - } - - hostname, _ := os.Hostname() - if hostname != "" { - return hostname - } - - // TODO Is there any stable identifier that can be used as a fallback? - return "" -} - -func Connect(ctx context.Context, opts Opts, invoker FunctionInvoker, logger *slog.Logger) error { - ch := &connectHandler{ - logger: logger, - invoker: invoker, - } - wp := NewWorkerPool(opts.WorkerConcurrency, ch.processExecutorRequest) - ch.workerPool = wp - - defer func() { - // TODO Push remaining messages to another destination for processing? - }() - - err := ch.Connect(ctx) - if err != nil { - return fmt.Errorf("could not establish connection: %w", err) - } - - return nil -} - func (h *connectHandler) Connect(ctx context.Context) error { signingKey := h.opts.HashedSigningKey if len(signingKey) == 0 { @@ -161,21 +128,27 @@ func (h *connectHandler) Connect(ctx context.Context) error { h.hostsManager = newHostsManager(hosts) - eg := errgroup.Group{} - notifyConnectDoneChan := make(chan connectReport) notifyConnectedChan := make(chan struct{}) initiateConnectionChan := make(chan struct{}) var attempts int + + eg := errgroup.Group{} + eg.Go(func() error { for { select { + // If the context is canceled, we should not attempt to reconnect case <-ctx.Done(): return nil + + // Reset attempts when connection succeeded case <-notifyConnectedChan: attempts = 0 continue + + // Handle connection done events case msg := <-notifyConnectDoneChan: h.logger.Error("connect failed", "err", err, "reconnect", msg.reconnect) @@ -183,6 +156,7 @@ func (h *connectHandler) Connect(ctx context.Context) error { return err } + // Some errors should be handled differently (e.g. auth failed) if msg.err != nil { closeErr := websocket.CloseError{} if errors.As(err, &closeErr) { @@ -273,133 +247,6 @@ func (h *connectHandler) Connect(ctx context.Context) error { return nil } -type connectionEstablishData struct { - hashedSigningKey []byte - numCpuCores int32 - totalMem int64 - marshaledFns []byte - marshaledCapabilities []byte - manualReadinessAck bool -} - -type preparedConnection struct { - ws *websocket.Conn - gatewayHost string -} - -func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGateways bool, data connectionEstablishData) (preparedConnection, bool, error) { - connectTimeout, cancelConnectTimeout := context.WithTimeout(ctx, 10*time.Second) - defer cancelConnectTimeout() - - gatewayHost := h.hostsManager.pickAvailableGateway() - if gatewayHost == "" { - h.hostsManager.resetGateways() - - return preparedConnection{}, allowResettingGateways, fmt.Errorf("no available gateway hosts") - } - - // Establish WebSocket connection to one of the gateways - ws, _, err := websocket.Dial(connectTimeout, gatewayHost, &websocket.DialOptions{ - Subprotocols: []string{ - types.GatewaySubProtocol, - }, - }) - if err != nil { - h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, false, fmt.Errorf("could not connect to gateway: %w", err) - } - - // Connection ID is unique per connection, reconnections should get a new ID - h.connectionId = ulid.MustNew(ulid.Now(), rand.Reader) - - h.logger.Debug("websocket connection established") - - // Wait for gateway hello message - { - initialMessageTimeout, cancelInitialTimeout := context.WithTimeout(ctx, 5*time.Second) - defer cancelInitialTimeout() - var helloMessage connectproto.ConnectMessage - err = wsproto.Read(initialMessageTimeout, ws, &helloMessage) - if err != nil { - h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, true, fmt.Errorf("did not receive gateway hello message: %w", err) - } - - if helloMessage.Kind != connectproto.GatewayMessageType_GATEWAY_HELLO { - h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, true, fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind) - } - - h.logger.Debug("received gateway hello message") - } - - // Send connect message - { - - apiOrigin := h.opts.APIBaseUrl - if h.opts.IsDev { - apiOrigin = h.opts.DevServerUrl - } - - data, err := proto.Marshal(&connectproto.WorkerConnectRequestData{ - SessionId: &connectproto.SessionIdentifier{ - BuildId: h.opts.BuildId, - InstanceId: h.instanceId(), - ConnectionId: h.connectionId.String(), - }, - AuthData: &connectproto.AuthData{ - HashedSigningKey: data.hashedSigningKey, - }, - AppName: h.opts.AppName, - Config: &connectproto.ConfigDetails{ - Capabilities: data.marshaledCapabilities, - Functions: data.marshaledFns, - ApiOrigin: apiOrigin, - }, - SystemAttributes: &connectproto.SystemAttributes{ - CpuCores: data.numCpuCores, - MemBytes: data.totalMem, - Os: runtime.GOOS, - }, - Environment: h.opts.Env, - Platform: h.opts.Platform, - SdkVersion: h.opts.SDKVersion, - SdkLanguage: h.opts.SDKLanguage, - WorkerManualReadinessAck: data.manualReadinessAck, - }) - if err != nil { - return preparedConnection{}, false, fmt.Errorf("could not serialize sdk connect message: %w", err) - } - - err = wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_CONNECT, - Payload: data, - }) - if err != nil { - return preparedConnection{}, true, fmt.Errorf("could not send initial message") - } - } - - // Wait for gateway ready message - { - connectionReadyTimeout, cancelConnectionReadyTimeout := context.WithTimeout(ctx, 20*time.Second) - defer cancelConnectionReadyTimeout() - var connectionReadyMsg connectproto.ConnectMessage - err = wsproto.Read(connectionReadyTimeout, ws, &connectionReadyMsg) - if err != nil { - return preparedConnection{}, true, fmt.Errorf("did not receive gateway connection ready message: %w", err) - } - - if connectionReadyMsg.Kind != connectproto.GatewayMessageType_GATEWAY_CONNECTION_READY { - return preparedConnection{}, true, fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind) - } - - h.logger.Debug("received gateway connection ready message") - } - - return preparedConnection{ws, gatewayHost}, false, nil -} - func (h *connectHandler) sendBufferedMessages(ws *websocket.Conn) error { processed := 0 for _, msg := range h.messageBuffer { @@ -422,230 +269,6 @@ func (h *connectHandler) sendBufferedMessages(ws *websocket.Conn) error { var errGatewayDraining = errors.New("gateway is draining") -func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) (reconnect bool, err error) { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - defer func() { - // TODO Do we need to include a reason here? If we only use this for unexpected disconnects, probably not - _ = ws.CloseNow() - }() - - go func() { - <-ctx.Done() - _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) - }() - - // Send buffered but unsent messages if connection was re-established - if len(h.messageBuffer) > 0 { - h.logger.Debug("sending buffered messages", "count", len(h.messageBuffer)) - err = h.sendBufferedMessages(ws) - if err != nil { - return true, fmt.Errorf("could not send buffered messages: %w", err) - } - } - - go func() { - heartbeatTicker := time.NewTicker(WorkerHeartbeatInterval) - defer heartbeatTicker.Stop() - for { - select { - case <-ctx.Done(): - return - case <-heartbeatTicker.C: - err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_HEARTBEAT, - }) - if err != nil { - h.logger.Error("failed to send worker heartbeat", "err", err) - } - } - - } - }() - - eg := errgroup.Group{} - eg.Go(func() error { - for { - var msg connectproto.ConnectMessage - err = wsproto.Read(context.Background(), ws, &msg) - if err != nil { - h.logger.Error("failed to read message", "err", err) - - // The connection may still be active, but for some reason we couldn't read the message - return err - } - - h.logger.Debug("received gateway request", "msg", &msg) - - switch msg.Kind { - case connectproto.GatewayMessageType_GATEWAY_CLOSING: - // Stop the read loop: We will not receive any further messages and should establish a new connection - // We can still use the old connection to send replies to the gateway - return errGatewayDraining - case connectproto.GatewayMessageType_GATEWAY_EXECUTOR_REQUEST: - // Handle invoke in a non-blocking way to allow for other messages to be processed - h.workerPool.Add(workerPoolMsg{ - msg: &msg, - ws: ws, - }) - default: - h.logger.Error("got unknown gateway request", "err", err) - continue - } - } - }) - - h.logger.Debug("waiting for read loop to end") - - // If read loop ends, this can be for two reasons - // - Connection loss (io.EOF), read loop terminated intentionally (CloseError), other error (unexpected) - // - Worker shutdown, parent context got cancelled - if err := eg.Wait(); err != nil && ctx.Err() == nil { - if errors.Is(err, errGatewayDraining) { - h.hostsManager.markDrainingGateway(gatewayHost) - - // Gateway is draining and will not accept new connections. - // We must reconnect to a different gateway, only then can we close the old connection. - waitUntilConnected, doneWaiting := context.WithCancel(context.Background()) - defer doneWaiting() - - // Intercept connected signal and pass it to the main goroutine - notifyConnectedInterceptChan := make(chan struct{}) - go func() { - <-notifyConnectedChan - notifyConnectedInterceptChan <- struct{}{} - doneWaiting() - }() - - // Establish new connection and pass close reports back to the main goroutine - h.connect(ctx, true, data, notifyConnectedInterceptChan, notifyConnectDoneChan) - - // Wait until the new connection is established before closing the old one - <-waitUntilConnected.Done() - - // By returning, we will close the old connection - return false, errGatewayDraining - } - - h.logger.Debug("read loop ended with error", "err", err) - - // In case the gateway intentionally closed the connection, we'll receive a close error - cerr := websocket.CloseError{} - if errors.As(err, &cerr) { - h.logger.Error("connection closed with reason", "reason", cerr.Reason) - - // Reconnect! - return true, fmt.Errorf("connection closed with reason %q: %w", cerr.Reason, cerr) - } - - // connection closed without reason - if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { - h.logger.Error("failed to read message from gateway, lost connection unexpectedly", "err", err) - return true, fmt.Errorf("connection closed unexpectedly: %w", cerr) - } - - // If this is not a worker shutdown, we should reconnect - return true, fmt.Errorf("connection closed unexpectedly: %w", ctx.Err()) - } - - // Perform graceful shutdown routine (context was cancelled) - - // Signal gateway that we won't process additional messages! - { - h.logger.Debug("sending worker pause message") - err := wsproto.Write(context.Background(), ws, &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_PAUSE, - }) - if err != nil { - // We should not exit here, as we're already in the shutdown routine - h.logger.Error("failed to serialize worker pause msg", "err", err) - } - } - - h.logger.Debug("waiting for in-progress requests to finish") - - // Wait until all in-progress requests are completed - h.workerPool.Wait() - - // Attempt to shut down connection if not already done - _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) - - return false, nil -} - -type connectReport struct { - reconnect bool - err error -} - -func (h *connectHandler) connect(ctx context.Context, allowResettingGateways bool, data connectionEstablishData, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) { - preparedConn, reconnect, err := h.prepareConnection(ctx, allowResettingGateways, data) - if err != nil { - notifyConnectDoneChan <- connectReport{ - reconnect: reconnect, - err: fmt.Errorf("could not establish connection: %w", err), - } - return - } - - notifyConnectedChan <- struct{}{} - - reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost, notifyConnectedChan, notifyConnectDoneChan) - if err != nil { - if errors.Is(err, errGatewayDraining) { - // if the gateway is draining, the original connection was closed, and we already reconnected inside handleConnection - return - } - - notifyConnectDoneChan <- connectReport{ - reconnect: reconnect, - err: fmt.Errorf("could not handle connection: %w", err), - } - return - } - - notifyConnectDoneChan <- connectReport{ - reconnect: reconnect, - err: nil, - } -} - -func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, handler func(ws *websocket.Conn) error) error { - // Prevent this connection from receiving work - data.manualReadinessAck = true - - maxAttempts := 4 - - var conn *websocket.Conn - var attempts int - for { - if attempts == maxAttempts { - return fmt.Errorf("could not establish connection after %d attempts", maxAttempts) - } - - ws, _, err := h.prepareConnection(context.Background(), true, data) - if err != nil { - attempts++ - continue - } - - conn = ws.ws - break - } - - defer func() { - _ = conn.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) - }() - - err := handler(conn) - if err != nil { - return err - } - - return nil -} - func (h *connectHandler) processExecutorRequest(msg workerPoolMsg) { defer h.workerPool.Done() @@ -673,182 +296,28 @@ func (h *connectHandler) processExecutorRequest(msg workerPoolMsg) { } } -func (h *connectHandler) handleInvokeMessage(ctx context.Context, ws *websocket.Conn, msg *connectproto.ConnectMessage) error { - resp, err := h.connectInvoke(ctx, ws, msg) - if err != nil { - h.logger.Error("failed to handle sdk request", "err", err) - // TODO Should we drop the connection? Continue receiving messages? - return fmt.Errorf("could not handle sdk request: %w", err) - } - - data, err := proto.Marshal(resp) - if err != nil { - h.logger.Error("failed to serialize sdk response", "err", err) - // TODO This should never happen; Signal that we should retry - return fmt.Errorf("could not serialize sdk response: %w", err) - } - - responseMessage := &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_REPLY, - Payload: data, +func (h *connectHandler) connectURLs() []string { + if h.opts.IsDev { + return []string{fmt.Sprintf("%s/connect", strings.Replace(h.opts.DevServerUrl, "http", "ws", 1))} } - err = wsproto.Write(ctx, ws, responseMessage) - if err != nil { - h.logger.Error("failed to send sdk response", "err", err) - - // Buffer message to retry - h.messageBufferLock.Lock() - h.messageBuffer = append(h.messageBuffer, responseMessage) - h.messageBufferLock.Unlock() - - return fmt.Errorf("could not send sdk response: %w", err) + if len(h.opts.ConnectUrls) > 0 { + return h.opts.ConnectUrls } return nil } -// connectInvoke is the counterpart to invoke for connect -func (h *connectHandler) connectInvoke(ctx context.Context, ws *websocket.Conn, msg *connectproto.ConnectMessage) (*connectproto.SDKResponse, error) { - body := connectproto.GatewayExecutorRequestData{} - if err := proto.Unmarshal(msg.Payload, &body); err != nil { - // TODO Should we send this back to the gateway? - h.logger.Error("error decoding gateway request data", "error", err) - return nil, fmt.Errorf("invalid gateway message data: %w", err) - } - - // Note: This still uses JSON - // TODO Replace with Protobuf - var request sdkrequest.Request - if err := json.Unmarshal(body.RequestPayload, &request); err != nil { - // TODO Should we send this back to the gateway? Previously this was a status code 400 public error with "malformed input" - h.logger.Error("error decoding sdk request", "error", err) - return nil, fmt.Errorf("invalid SDK request payload: %w", err) - } - - ackPayload, err := proto.Marshal(&connectproto.WorkerRequestAckData{ - RequestId: body.RequestId, - AppId: body.AppId, - FunctionSlug: body.FunctionSlug, - StepId: body.StepId, - }) - if err != nil { - h.logger.Error("error marshaling request ack", "error", err) - return nil, publicerr.Error{ - Message: "malformed input", - Status: 400, - } - } - - // Ack message - // If we're shutting down (context is canceled) we will not ack, which is desired! - if err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_REQUEST_ACK, - Payload: ackPayload, - }); err != nil { - h.logger.Error("error sending request ack", "error", err) - return nil, publicerr.Error{ - Message: "failed to ack worker request", - Status: 400, - } - } - - // TODO Should we wait for a gateway response before starting to process? What if the gateway fails acking and we start too early? - // This should not happen but could lead to double processing of the same message - - if request.UseAPI { - // TODO: implement this - // retrieve data from API - // request.Steps = - // request.Events = - _ = 0 // no-op to avoid linter error - } - - var stepId *string - if body.StepId != nil && *body.StepId != "step" { - stepId = body.StepId - } - - // Invoke function, always complete regardless of - resp, ops, err := h.invoker.InvokeFunction(context.Background(), body.FunctionSlug, stepId, request) - - // NOTE: When triggering step errors, we should have an OpcodeStepError - // within ops alongside an error. We can safely ignore that error, as it's - // only used for checking whether the step used a NoRetryError or RetryAtError - // - // For that reason, we check those values first. - noRetry := sdkerrors.IsNoRetryError(err) - retryAt := sdkerrors.GetRetryAtTime(err) - if len(ops) == 1 && ops[0].Op == enums.OpcodeStepError { - // Now we've handled error types we can ignore step - // errors safely. - err = nil - } - - // Now that we've handled the OpcodeStepError, if we *still* ahve - // a StepError kind returned from a function we must have an unhandled - // step error. This is a NonRetryableError, as the most likely code is: - // - // _, err := step.Run(ctx, func() (any, error) { return fmt.Errorf("") }) - // if err != nil { - // return err - // } - if sdkerrors.IsStepError(err) { - err = fmt.Errorf("Unhandled step error: %s", err) - noRetry = true - } - - // These may be added even for 2xx codes with step errors. - var retryAfterVal *string - if retryAt != nil { - formatted := retryAt.Format(time.RFC3339) - retryAfterVal = &formatted - } - - if err != nil { - h.logger.Error("error calling function", "error", err) - return &connectproto.SDKResponse{ - RequestId: body.RequestId, - Status: connectproto.SDKResponseStatus_ERROR, - Body: []byte(fmt.Sprintf("error calling function: %s", err.Error())), - NoRetry: noRetry, - RetryAfter: retryAfterVal, - }, nil - } - - if len(ops) > 0 { - // Note: This still uses JSON - // TODO Replace with Protobuf - serializedOps, err := json.Marshal(ops) - if err != nil { - return nil, fmt.Errorf("could not serialize ops: %w", err) - } - - // Return the function opcode returned here so that we can re-invoke this - // function and manage state appropriately. Any opcode here takes precedence - // over function return values as the function has not yet finished. - return &connectproto.SDKResponse{ - RequestId: body.RequestId, - Status: connectproto.SDKResponseStatus_NOT_COMPLETED, - Body: serializedOps, - NoRetry: noRetry, - RetryAfter: retryAfterVal, - }, nil +func (h *connectHandler) instanceId() string { + if h.opts.InstanceId != nil { + return *h.opts.InstanceId } - // Note: This still uses JSON - // TODO Replace with Protobuf - serializedResp, err := json.Marshal(resp) - if err != nil { - return nil, fmt.Errorf("could not serialize resp: %w", err) + hostname, _ := os.Hostname() + if hostname != "" { + return hostname } - // Return the function response. - return &connectproto.SDKResponse{ - RequestId: body.RequestId, - Status: connectproto.SDKResponseStatus_DONE, - Body: serializedResp, - NoRetry: noRetry, - RetryAfter: retryAfterVal, - }, nil + // TODO Is there any stable identifier that can be used as a fallback? + return "" } diff --git a/connect/invoke.go b/connect/invoke.go new file mode 100644 index 00000000..9a797eed --- /dev/null +++ b/connect/invoke.go @@ -0,0 +1,196 @@ +package connect + +import ( + "context" + "encoding/json" + "fmt" + "github.com/coder/websocket" + "github.com/inngest/inngest/pkg/connect/wsproto" + "github.com/inngest/inngest/pkg/enums" + "github.com/inngest/inngest/pkg/publicerr" + connectproto "github.com/inngest/inngest/proto/gen/connect/v1" + sdkerrors "github.com/inngest/inngestgo/errors" + "github.com/inngest/inngestgo/internal/sdkrequest" + "google.golang.org/protobuf/proto" + "time" +) + +func (h *connectHandler) handleInvokeMessage(ctx context.Context, ws *websocket.Conn, msg *connectproto.ConnectMessage) error { + resp, err := h.connectInvoke(ctx, ws, msg) + if err != nil { + h.logger.Error("failed to handle sdk request", "err", err) + // TODO Should we drop the connection? Continue receiving messages? + return fmt.Errorf("could not handle sdk request: %w", err) + } + + data, err := proto.Marshal(resp) + if err != nil { + h.logger.Error("failed to serialize sdk response", "err", err) + // TODO This should never happen; Signal that we should retry + return fmt.Errorf("could not serialize sdk response: %w", err) + } + + responseMessage := &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_REPLY, + Payload: data, + } + + err = wsproto.Write(ctx, ws, responseMessage) + if err != nil { + h.logger.Error("failed to send sdk response", "err", err) + + // Buffer message to retry + h.messageBufferLock.Lock() + h.messageBuffer = append(h.messageBuffer, responseMessage) + h.messageBufferLock.Unlock() + + return fmt.Errorf("could not send sdk response: %w", err) + } + + return nil +} + +// connectInvoke is the counterpart to invoke for connect +func (h *connectHandler) connectInvoke(ctx context.Context, ws *websocket.Conn, msg *connectproto.ConnectMessage) (*connectproto.SDKResponse, error) { + body := connectproto.GatewayExecutorRequestData{} + if err := proto.Unmarshal(msg.Payload, &body); err != nil { + // TODO Should we send this back to the gateway? + h.logger.Error("error decoding gateway request data", "error", err) + return nil, fmt.Errorf("invalid gateway message data: %w", err) + } + + // Note: This still uses JSON + // TODO Replace with Protobuf + var request sdkrequest.Request + if err := json.Unmarshal(body.RequestPayload, &request); err != nil { + // TODO Should we send this back to the gateway? Previously this was a status code 400 public error with "malformed input" + h.logger.Error("error decoding sdk request", "error", err) + return nil, fmt.Errorf("invalid SDK request payload: %w", err) + } + + ackPayload, err := proto.Marshal(&connectproto.WorkerRequestAckData{ + RequestId: body.RequestId, + AppId: body.AppId, + FunctionSlug: body.FunctionSlug, + StepId: body.StepId, + }) + if err != nil { + h.logger.Error("error marshaling request ack", "error", err) + return nil, publicerr.Error{ + Message: "malformed input", + Status: 400, + } + } + + // Ack message + // If we're shutting down (context is canceled) we will not ack, which is desired! + if err := wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_REQUEST_ACK, + Payload: ackPayload, + }); err != nil { + h.logger.Error("error sending request ack", "error", err) + return nil, publicerr.Error{ + Message: "failed to ack worker request", + Status: 400, + } + } + + // TODO Should we wait for a gateway response before starting to process? What if the gateway fails acking and we start too early? + // This should not happen but could lead to double processing of the same message + + if request.UseAPI { + // TODO: implement this + // retrieve data from API + // request.Steps = + // request.Events = + _ = 0 // no-op to avoid linter error + } + + var stepId *string + if body.StepId != nil && *body.StepId != "step" { + stepId = body.StepId + } + + // Invoke function, always complete regardless of + resp, ops, err := h.invoker.InvokeFunction(context.Background(), body.FunctionSlug, stepId, request) + + // NOTE: When triggering step errors, we should have an OpcodeStepError + // within ops alongside an error. We can safely ignore that error, as it's + // only used for checking whether the step used a NoRetryError or RetryAtError + // + // For that reason, we check those values first. + noRetry := sdkerrors.IsNoRetryError(err) + retryAt := sdkerrors.GetRetryAtTime(err) + if len(ops) == 1 && ops[0].Op == enums.OpcodeStepError { + // Now we've handled error types we can ignore step + // errors safely. + err = nil + } + + // Now that we've handled the OpcodeStepError, if we *still* ahve + // a StepError kind returned from a function we must have an unhandled + // step error. This is a NonRetryableError, as the most likely code is: + // + // _, err := step.Run(ctx, func() (any, error) { return fmt.Errorf("") }) + // if err != nil { + // return err + // } + if sdkerrors.IsStepError(err) { + err = fmt.Errorf("Unhandled step error: %s", err) + noRetry = true + } + + // These may be added even for 2xx codes with step errors. + var retryAfterVal *string + if retryAt != nil { + formatted := retryAt.Format(time.RFC3339) + retryAfterVal = &formatted + } + + if err != nil { + h.logger.Error("error calling function", "error", err) + return &connectproto.SDKResponse{ + RequestId: body.RequestId, + Status: connectproto.SDKResponseStatus_ERROR, + Body: []byte(fmt.Sprintf("error calling function: %s", err.Error())), + NoRetry: noRetry, + RetryAfter: retryAfterVal, + }, nil + } + + if len(ops) > 0 { + // Note: This still uses JSON + // TODO Replace with Protobuf + serializedOps, err := json.Marshal(ops) + if err != nil { + return nil, fmt.Errorf("could not serialize ops: %w", err) + } + + // Return the function opcode returned here so that we can re-invoke this + // function and manage state appropriately. Any opcode here takes precedence + // over function return values as the function has not yet finished. + return &connectproto.SDKResponse{ + RequestId: body.RequestId, + Status: connectproto.SDKResponseStatus_NOT_COMPLETED, + Body: serializedOps, + NoRetry: noRetry, + RetryAfter: retryAfterVal, + }, nil + } + + // Note: This still uses JSON + // TODO Replace with Protobuf + serializedResp, err := json.Marshal(resp) + if err != nil { + return nil, fmt.Errorf("could not serialize resp: %w", err) + } + + // Return the function response. + return &connectproto.SDKResponse{ + RequestId: body.RequestId, + Status: connectproto.SDKResponseStatus_DONE, + Body: serializedResp, + NoRetry: noRetry, + RetryAfter: retryAfterVal, + }, nil +} diff --git a/handler.go b/handler.go index 484407fa..d127ddbd 100644 --- a/handler.go +++ b/handler.go @@ -313,8 +313,6 @@ type handler struct { funcs []ServableFunction // lock prevents reading the function maps while serving l sync.RWMutex - - useConnect bool } func (h *handler) SetOptions(opts HandlerOpts) Handler { From 36fb7858d6133ac6c85982f3d29698dd5d28676a Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 16:10:28 +0100 Subject: [PATCH 07/16] Split out handshake logic --- connect/connection.go | 102 ++++++------------------------------------ connect/handler.go | 19 +++++--- connect/handshake.go | 99 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 94 deletions(-) create mode 100644 connect/handshake.go diff --git a/connect/connection.go b/connect/connection.go index 87907d35..68f77ae8 100644 --- a/connect/connection.go +++ b/connect/connection.go @@ -11,10 +11,8 @@ import ( connectproto "github.com/inngest/inngest/proto/gen/connect/v1" "github.com/oklog/ulid/v2" "golang.org/x/sync/errgroup" - "google.golang.org/protobuf/proto" "io" "net" - "runtime" "time" ) @@ -24,6 +22,7 @@ type connectReport struct { } func (h *connectHandler) connect(ctx context.Context, allowResettingGateways bool, data connectionEstablishData, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) { + // Set up connection (including connect handshake protocol) preparedConn, reconnect, err := h.prepareConnection(ctx, allowResettingGateways, data) if err != nil { notifyConnectDoneChan <- connectReport{ @@ -33,8 +32,10 @@ func (h *connectHandler) connect(ctx context.Context, allowResettingGateways boo return } + // Notify that the connection was established notifyConnectedChan <- struct{}{} + // Set up connection lifecycle logic (receiving messages, handling requests, etc.) reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost, notifyConnectedChan, notifyConnectDoneChan) if err != nil { if errors.Is(err, errGatewayDraining) { @@ -65,8 +66,9 @@ type connectionEstablishData struct { } type preparedConnection struct { - ws *websocket.Conn - gatewayHost string + ws *websocket.Conn + gatewayHost string + connectionId string } func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGateways bool, data connectionEstablishData) (preparedConnection, bool, error) { @@ -92,94 +94,16 @@ func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGa } // Connection ID is unique per connection, reconnections should get a new ID - h.connectionId = ulid.MustNew(ulid.Now(), rand.Reader) + connectionId := ulid.MustNew(ulid.Now(), rand.Reader) h.logger.Debug("websocket connection established") - // Wait for gateway hello message - { - initialMessageTimeout, cancelInitialTimeout := context.WithTimeout(ctx, 5*time.Second) - defer cancelInitialTimeout() - var helloMessage connectproto.ConnectMessage - err = wsproto.Read(initialMessageTimeout, ws, &helloMessage) - if err != nil { - h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, true, fmt.Errorf("did not receive gateway hello message: %w", err) - } - - if helloMessage.Kind != connectproto.GatewayMessageType_GATEWAY_HELLO { - h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, true, fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind) - } - - h.logger.Debug("received gateway hello message") - } - - // Send connect message - { - - apiOrigin := h.opts.APIBaseUrl - if h.opts.IsDev { - apiOrigin = h.opts.DevServerUrl - } - - data, err := proto.Marshal(&connectproto.WorkerConnectRequestData{ - SessionId: &connectproto.SessionIdentifier{ - BuildId: h.opts.BuildId, - InstanceId: h.instanceId(), - ConnectionId: h.connectionId.String(), - }, - AuthData: &connectproto.AuthData{ - HashedSigningKey: data.hashedSigningKey, - }, - AppName: h.opts.AppName, - Config: &connectproto.ConfigDetails{ - Capabilities: data.marshaledCapabilities, - Functions: data.marshaledFns, - ApiOrigin: apiOrigin, - }, - SystemAttributes: &connectproto.SystemAttributes{ - CpuCores: data.numCpuCores, - MemBytes: data.totalMem, - Os: runtime.GOOS, - }, - Environment: h.opts.Env, - Platform: h.opts.Platform, - SdkVersion: h.opts.SDKVersion, - SdkLanguage: h.opts.SDKLanguage, - WorkerManualReadinessAck: data.manualReadinessAck, - }) - if err != nil { - return preparedConnection{}, false, fmt.Errorf("could not serialize sdk connect message: %w", err) - } - - err = wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ - Kind: connectproto.GatewayMessageType_WORKER_CONNECT, - Payload: data, - }) - if err != nil { - return preparedConnection{}, true, fmt.Errorf("could not send initial message") - } - } - - // Wait for gateway ready message - { - connectionReadyTimeout, cancelConnectionReadyTimeout := context.WithTimeout(ctx, 20*time.Second) - defer cancelConnectionReadyTimeout() - var connectionReadyMsg connectproto.ConnectMessage - err = wsproto.Read(connectionReadyTimeout, ws, &connectionReadyMsg) - if err != nil { - return preparedConnection{}, true, fmt.Errorf("did not receive gateway connection ready message: %w", err) - } - - if connectionReadyMsg.Kind != connectproto.GatewayMessageType_GATEWAY_CONNECTION_READY { - return preparedConnection{}, true, fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind) - } - - h.logger.Debug("received gateway connection ready message") + reconnect, err := h.performConnectHandshake(ctx, connectionId.String(), ws, gatewayHost, data) + if err != nil { + return preparedConnection{}, reconnect, fmt.Errorf("could not perform connect handshake: %w", err) } - return preparedConnection{ws, gatewayHost}, false, nil + return preparedConnection{ws, gatewayHost, connectionId.String()}, false, nil } func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) (reconnect bool, err error) { @@ -187,10 +111,12 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs defer cancel() defer func() { - // TODO Do we need to include a reason here? If we only use this for unexpected disconnects, probably not + // This is a fallback safeguard to always close the WebSocket connection at the end of the function + // Usually, we provide a specific reason, so this is only necessary for unhandled errors _ = ws.CloseNow() }() + // When shutting down the worker, close the connection with a reason go func() { <-ctx.Done() _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) diff --git a/connect/handler.go b/connect/handler.go index 519aabcc..7ef564ed 100644 --- a/connect/handler.go +++ b/connect/handler.go @@ -12,7 +12,6 @@ import ( "github.com/inngest/inngest/pkg/syscode" connectproto "github.com/inngest/inngest/proto/gen/connect/v1" "github.com/inngest/inngestgo/internal/sdkrequest" - "github.com/oklog/ulid/v2" "github.com/pbnjay/memory" "golang.org/x/exp/slog" "golang.org/x/sync/errgroup" @@ -32,6 +31,7 @@ func Connect(ctx context.Context, opts Opts, invoker FunctionInvoker, logger *sl ch := &connectHandler{ logger: logger, invoker: invoker, + opts: opts, } wp := NewWorkerPool(opts.WorkerConcurrency, ch.processExecutorRequest) ch.workerPool = wp @@ -84,8 +84,6 @@ type connectHandler struct { logger *slog.Logger - connectionId ulid.ULID - messageBuffer []*connectproto.ConnectMessage messageBufferLock sync.Mutex @@ -128,14 +126,22 @@ func (h *connectHandler) Connect(ctx context.Context) error { h.hostsManager = newHostsManager(hosts) + // Notify when connect finishes (either with an error or because the context got canceled) notifyConnectDoneChan := make(chan connectReport) + + // Notify when connection is established notifyConnectedChan := make(chan struct{}) + + // Channel to imperatively initiate a connection initiateConnectionChan := make(chan struct{}) var attempts int + // We construct a connection loop, which will attempt to reconnect on failure + // Instead of doing a simple, synchronous loop, we use channels to communicate connection status changes, + // allowing to instantiate a new connection while the previous one is still running. + // This is crucial for handling gateway draining scenarios. eg := errgroup.Group{} - eg.Go(func() error { for { select { @@ -175,13 +181,11 @@ func (h *connectHandler) Connect(ctx context.Context) error { auth = authContext{hashedSigningKey: signingKeyFallback, fallback: true} initiateConnectionChan <- struct{}{} - continue // Retry on the following error codes case syscode.CodeConnectGatewayClosing, syscode.CodeConnectInternal, syscode.CodeConnectWorkerHelloTimeout: initiateConnectionChan <- struct{}{} - continue default: @@ -192,6 +196,7 @@ func (h *connectHandler) Connect(ctx context.Context) error { } initiateConnectionChan <- struct{}{} + continue case <-initiateConnectionChan: } @@ -211,8 +216,10 @@ func (h *connectHandler) Connect(ctx context.Context) error { } }) + // Initiate the first connection initiateConnectionChan <- struct{}{} + // Wait until connection loop finishes if err := eg.Wait(); err != nil { return fmt.Errorf("could not establish connection: %w", err) } diff --git a/connect/handshake.go b/connect/handshake.go new file mode 100644 index 00000000..1135d1eb --- /dev/null +++ b/connect/handshake.go @@ -0,0 +1,99 @@ +package connect + +import ( + "context" + "fmt" + "github.com/coder/websocket" + "github.com/inngest/inngest/pkg/connect/wsproto" + connectproto "github.com/inngest/inngest/proto/gen/connect/v1" + "google.golang.org/protobuf/proto" + "runtime" + "time" +) + +func (h *connectHandler) performConnectHandshake(ctx context.Context, connectionId string, ws *websocket.Conn, gatewayHost string, data connectionEstablishData) (bool, error) { + // Wait for gateway hello message + { + initialMessageTimeout, cancelInitialTimeout := context.WithTimeout(ctx, 5*time.Second) + defer cancelInitialTimeout() + var helloMessage connectproto.ConnectMessage + err := wsproto.Read(initialMessageTimeout, ws, &helloMessage) + if err != nil { + h.hostsManager.markUnreachableGateway(gatewayHost) + return true, fmt.Errorf("did not receive gateway hello message: %w", err) + } + + if helloMessage.Kind != connectproto.GatewayMessageType_GATEWAY_HELLO { + h.hostsManager.markUnreachableGateway(gatewayHost) + return true, fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind) + } + + h.logger.Debug("received gateway hello message") + } + + // Send connect message + { + + apiOrigin := h.opts.APIBaseUrl + if h.opts.IsDev { + apiOrigin = h.opts.DevServerUrl + } + + data, err := proto.Marshal(&connectproto.WorkerConnectRequestData{ + SessionId: &connectproto.SessionIdentifier{ + BuildId: h.opts.BuildId, + InstanceId: h.instanceId(), + ConnectionId: connectionId, + }, + AuthData: &connectproto.AuthData{ + HashedSigningKey: data.hashedSigningKey, + }, + AppName: h.opts.AppName, + Config: &connectproto.ConfigDetails{ + Capabilities: data.marshaledCapabilities, + Functions: data.marshaledFns, + ApiOrigin: apiOrigin, + }, + SystemAttributes: &connectproto.SystemAttributes{ + CpuCores: data.numCpuCores, + MemBytes: data.totalMem, + Os: runtime.GOOS, + }, + Environment: h.opts.Env, + Platform: h.opts.Platform, + SdkVersion: h.opts.SDKVersion, + SdkLanguage: h.opts.SDKLanguage, + WorkerManualReadinessAck: data.manualReadinessAck, + }) + if err != nil { + return false, fmt.Errorf("could not serialize sdk connect message: %w", err) + } + + err = wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ + Kind: connectproto.GatewayMessageType_WORKER_CONNECT, + Payload: data, + }) + if err != nil { + return true, fmt.Errorf("could not send initial message") + } + } + + // Wait for gateway ready message + { + connectionReadyTimeout, cancelConnectionReadyTimeout := context.WithTimeout(ctx, 20*time.Second) + defer cancelConnectionReadyTimeout() + var connectionReadyMsg connectproto.ConnectMessage + err := wsproto.Read(connectionReadyTimeout, ws, &connectionReadyMsg) + if err != nil { + return true, fmt.Errorf("did not receive gateway connection ready message: %w", err) + } + + if connectionReadyMsg.Kind != connectproto.GatewayMessageType_GATEWAY_CONNECTION_READY { + return true, fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind) + } + + h.logger.Debug("received gateway connection ready message") + } + + return false, nil +} From d3ecedee5ad219a8b3d81c96f70807a521104392 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 16:13:19 +0100 Subject: [PATCH 08/16] Connect in dedicated goroutine --- connect/connection.go | 2 +- connect/handler.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/connect/connection.go b/connect/connection.go index 68f77ae8..0cf28a55 100644 --- a/connect/connection.go +++ b/connect/connection.go @@ -205,7 +205,7 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs }() // Establish new connection and pass close reports back to the main goroutine - h.connect(ctx, true, data, notifyConnectedInterceptChan, notifyConnectDoneChan) + go h.connect(ctx, true, data, notifyConnectedInterceptChan, notifyConnectDoneChan) // Wait until the new connection is established before closing the old one <-waitUntilConnected.Done() diff --git a/connect/handler.go b/connect/handler.go index 7ef564ed..1b3c32cd 100644 --- a/connect/handler.go +++ b/connect/handler.go @@ -206,7 +206,7 @@ func (h *connectHandler) Connect(ctx context.Context) error { attempts++ - h.connect(ctx, false, connectionEstablishData{ + go h.connect(ctx, false, connectionEstablishData{ hashedSigningKey: auth.hashedSigningKey, numCpuCores: int32(numCpuCores), totalMem: int64(totalMem), From a7bddb520b7647c14e540610e576606e362db64d Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 16:15:04 +0100 Subject: [PATCH 09/16] Update connection.go --- connect/connection.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/connect/connection.go b/connect/connection.go index 0cf28a55..e4627fb3 100644 --- a/connect/connection.go +++ b/connect/connection.go @@ -77,8 +77,10 @@ func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGa gatewayHost := h.hostsManager.pickAvailableGateway() if gatewayHost == "" { + // All gateways have been tried, reset the internal state to retry h.hostsManager.resetGateways() + // Only reconnect if allowResettingGateways is true return preparedConnection{}, allowResettingGateways, fmt.Errorf("no available gateway hosts") } @@ -273,7 +275,7 @@ func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, h return fmt.Errorf("could not establish connection after %d attempts", maxAttempts) } - ws, _, err := h.prepareConnection(context.Background(), true, data) + ws, _, err := h.prepareConnection(context.Background(), false, data) if err != nil { attempts++ continue From efe6ed478471772c394bb29d8f14f12fa06696d4 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 16:32:44 +0100 Subject: [PATCH 10/16] fix goroutine deadlock When using the same goroutine to handle channel selects and sending to an unbuffered channel, there's no way for the channel receive to work. This is fixed by not using a channel for control flow from error group to error group. This way, the same goroutine can be used. --- connect/connection.go | 29 +++++++++++++++++++---------- connect/handler.go | 24 +++++++++++------------- handler.go | 2 +- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/connect/connection.go b/connect/connection.go index e4627fb3..0c267eb6 100644 --- a/connect/connection.go +++ b/connect/connection.go @@ -21,10 +21,12 @@ type connectReport struct { err error } -func (h *connectHandler) connect(ctx context.Context, allowResettingGateways bool, data connectionEstablishData, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) { +func (h *connectHandler) connect(ctx context.Context, data connectionEstablishData, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) { // Set up connection (including connect handshake protocol) - preparedConn, reconnect, err := h.prepareConnection(ctx, allowResettingGateways, data) + preparedConn, reconnect, err := h.prepareConnection(ctx, data) if err != nil { + h.logger.Error("could not establish connection", "err", err) + notifyConnectDoneChan <- connectReport{ reconnect: reconnect, err: fmt.Errorf("could not establish connection: %w", err), @@ -38,6 +40,8 @@ func (h *connectHandler) connect(ctx context.Context, allowResettingGateways boo // Set up connection lifecycle logic (receiving messages, handling requests, etc.) reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost, notifyConnectedChan, notifyConnectDoneChan) if err != nil { + h.logger.Error("could not handle connection", "err", err) + if errors.Is(err, errGatewayDraining) { // if the gateway is draining, the original connection was closed, and we already reconnected inside handleConnection return @@ -71,7 +75,7 @@ type preparedConnection struct { connectionId string } -func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGateways bool, data connectionEstablishData) (preparedConnection, bool, error) { +func (h *connectHandler) prepareConnection(ctx context.Context, data connectionEstablishData) (preparedConnection, bool, error) { connectTimeout, cancelConnectTimeout := context.WithTimeout(ctx, 10*time.Second) defer cancelConnectTimeout() @@ -80,8 +84,7 @@ func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGa // All gateways have been tried, reset the internal state to retry h.hostsManager.resetGateways() - // Only reconnect if allowResettingGateways is true - return preparedConnection{}, allowResettingGateways, fmt.Errorf("no available gateway hosts") + return preparedConnection{}, true, fmt.Errorf("no available gateway hosts") } // Establish WebSocket connection to one of the gateways @@ -92,13 +95,13 @@ func (h *connectHandler) prepareConnection(ctx context.Context, allowResettingGa }) if err != nil { h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, false, fmt.Errorf("could not connect to gateway: %w", err) + return preparedConnection{}, true, fmt.Errorf("could not connect to gateway: %w", err) } // Connection ID is unique per connection, reconnections should get a new ID connectionId := ulid.MustNew(ulid.Now(), rand.Reader) - h.logger.Debug("websocket connection established") + h.logger.Debug("websocket connection established", "gateway_host", gatewayHost) reconnect, err := h.performConnectHandshake(ctx, connectionId.String(), ws, gatewayHost, data) if err != nil { @@ -207,10 +210,16 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs }() // Establish new connection and pass close reports back to the main goroutine - go h.connect(ctx, true, data, notifyConnectedInterceptChan, notifyConnectDoneChan) + go h.connect(context.Background(), data, notifyConnectedInterceptChan, notifyConnectDoneChan) + + cancel() // Wait until the new connection is established before closing the old one - <-waitUntilConnected.Done() + select { + case <-waitUntilConnected.Done(): + case <-time.After(10 * time.Second): + h.logger.Error("timed out waiting for new connection to be established") + } // By returning, we will close the old connection return false, errGatewayDraining @@ -275,7 +284,7 @@ func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, h return fmt.Errorf("could not establish connection after %d attempts", maxAttempts) } - ws, _, err := h.prepareConnection(context.Background(), false, data) + ws, _, err := h.prepareConnection(context.Background(), data) if err != nil { attempts++ continue diff --git a/connect/handler.go b/connect/handler.go index 1b3c32cd..73eb15cb 100644 --- a/connect/handler.go +++ b/connect/handler.go @@ -13,9 +13,9 @@ import ( connectproto "github.com/inngest/inngest/proto/gen/connect/v1" "github.com/inngest/inngestgo/internal/sdkrequest" "github.com/pbnjay/memory" - "golang.org/x/exp/slog" "golang.org/x/sync/errgroup" "io" + "log/slog" "os" "runtime" "strings" @@ -180,14 +180,11 @@ func (h *connectHandler) Connect(ctx context.Context) error { auth = authContext{hashedSigningKey: signingKeyFallback, fallback: true} - initiateConnectionChan <- struct{}{} - continue + // continue to reconnect logic // Retry on the following error codes case syscode.CodeConnectGatewayClosing, syscode.CodeConnectInternal, syscode.CodeConnectWorkerHelloTimeout: - initiateConnectionChan <- struct{}{} - continue - + // continue to reconnect logic default: // If we received a reason that's non-retriable, stop here. return fmt.Errorf("connect failed with error code %q", closeErr.Reason) @@ -195,8 +192,9 @@ func (h *connectHandler) Connect(ctx context.Context) error { } } - initiateConnectionChan <- struct{}{} - continue + // continue to reconnect logic + h.logger.Debug("reconnecting", "attempts", attempts) + case <-initiateConnectionChan: } @@ -206,7 +204,7 @@ func (h *connectHandler) Connect(ctx context.Context) error { attempts++ - go h.connect(ctx, false, connectionEstablishData{ + go h.connect(ctx, connectionEstablishData{ hashedSigningKey: auth.hashedSigningKey, numCpuCores: int32(numCpuCores), totalMem: int64(totalMem), @@ -304,14 +302,14 @@ func (h *connectHandler) processExecutorRequest(msg workerPoolMsg) { } func (h *connectHandler) connectURLs() []string { - if h.opts.IsDev { - return []string{fmt.Sprintf("%s/connect", strings.Replace(h.opts.DevServerUrl, "http", "ws", 1))} - } - if len(h.opts.ConnectUrls) > 0 { return h.opts.ConnectUrls } + if h.opts.IsDev { + return []string{fmt.Sprintf("%s/connect", strings.Replace(h.opts.DevServerUrl, "http", "ws", 1))} + } + return nil } diff --git a/handler.go b/handler.go index d127ddbd..68078148 100644 --- a/handler.go +++ b/handler.go @@ -24,7 +24,7 @@ import ( "github.com/inngest/inngestgo/internal/sdkrequest" "github.com/inngest/inngestgo/internal/types" "github.com/inngest/inngestgo/step" - "golang.org/x/exp/slog" + "log/slog" ) var ( From ad4ccfed753ccaa4bfccacdd26fe897737994095 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 16:56:49 +0100 Subject: [PATCH 11/16] update vendor --- go.mod | 13 +- go.sum | 26 + vendor/golang.org/x/exp/slog/attr.go | 102 ---- vendor/golang.org/x/exp/slog/doc.go | 316 ---------- vendor/golang.org/x/exp/slog/handler.go | 577 ------------------ .../x/exp/slog/internal/buffer/buffer.go | 84 --- .../x/exp/slog/internal/ignorepc.go | 9 - vendor/golang.org/x/exp/slog/json_handler.go | 336 ---------- vendor/golang.org/x/exp/slog/level.go | 201 ------ vendor/golang.org/x/exp/slog/logger.go | 343 ----------- vendor/golang.org/x/exp/slog/noplog.bench | 36 -- vendor/golang.org/x/exp/slog/record.go | 207 ------- vendor/golang.org/x/exp/slog/text_handler.go | 161 ----- vendor/golang.org/x/exp/slog/value.go | 456 -------------- vendor/golang.org/x/exp/slog/value_119.go | 53 -- vendor/golang.org/x/exp/slog/value_120.go | 39 -- vendor/modules.txt | 5 +- 17 files changed, 39 insertions(+), 2925 deletions(-) delete mode 100644 vendor/golang.org/x/exp/slog/attr.go delete mode 100644 vendor/golang.org/x/exp/slog/doc.go delete mode 100644 vendor/golang.org/x/exp/slog/handler.go delete mode 100644 vendor/golang.org/x/exp/slog/internal/buffer/buffer.go delete mode 100644 vendor/golang.org/x/exp/slog/internal/ignorepc.go delete mode 100644 vendor/golang.org/x/exp/slog/json_handler.go delete mode 100644 vendor/golang.org/x/exp/slog/level.go delete mode 100644 vendor/golang.org/x/exp/slog/logger.go delete mode 100644 vendor/golang.org/x/exp/slog/noplog.bench delete mode 100644 vendor/golang.org/x/exp/slog/record.go delete mode 100644 vendor/golang.org/x/exp/slog/text_handler.go delete mode 100644 vendor/golang.org/x/exp/slog/value.go delete mode 100644 vendor/golang.org/x/exp/slog/value_119.go delete mode 100644 vendor/golang.org/x/exp/slog/value_120.go diff --git a/go.mod b/go.mod index 304f5732..498f876a 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gosimple/slug v1.12.0 github.com/gowebpki/jcs v1.0.0 - github.com/inngest/inngest v1.2.1-0.20241121162138-fa921e44acf1 + github.com/inngest/inngest v1.3.1-0.20241122155305-f9a9e9ca9b15 github.com/oklog/ulid/v2 v2.1.0 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/sashabaranov/go-openai v1.35.6 @@ -19,12 +19,16 @@ require ( ) require ( + github.com/99designs/gqlgen v0.17.27 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/agnivade/levenshtein v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dmarkham/enumer v1.5.8 // indirect github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -46,6 +50,7 @@ require ( github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/ohler55/ojg v1.24.1 // indirect + github.com/pascaldekloe/name v1.0.1 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -55,6 +60,7 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/zerolog v1.26.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.4.1 // indirect @@ -64,6 +70,9 @@ require ( github.com/stoewer/go-strcase v1.3.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/tidwall/btree v1.7.0 // indirect + github.com/urfave/cli/v2 v2.25.1 // indirect + github.com/vektah/gqlparser/v2 v2.5.15 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect @@ -80,10 +89,12 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.28.0 // indirect + golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/grpc v1.67.1 // indirect diff --git a/go.sum b/go.sum index e1fb9e0b..0e4ca995 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,12 @@ +github.com/99designs/gqlgen v0.17.27 h1:XPsaZiWY1lL2qqVYtBt37GzkyX7bBiVvda7k1buC/Ao= +github.com/99designs/gqlgen v0.17.27/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -11,9 +16,14 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM= +github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak= github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY= github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -51,6 +61,8 @@ github.com/inngest/expr v0.0.0-20241106234328-863dff7deec0 h1:cqaGD0mx745BYyVGZ3 github.com/inngest/expr v0.0.0-20241106234328-863dff7deec0/go.mod h1:0Dllw9clwlMWWxfiSsHY9VdE+Zjt/8SVCMxK9r39aKE= github.com/inngest/inngest v1.2.1-0.20241121162138-fa921e44acf1 h1:yVbhmibleJZrXqT4IUIqaA+tAAf4iCmVi2eIct8QHwI= github.com/inngest/inngest v1.2.1-0.20241121162138-fa921e44acf1/go.mod h1:+Nl49tpdy+gRzEYHgEb3eNV7aheh8iXyP15XKc+nVU4= +github.com/inngest/inngest v1.3.1-0.20241122155305-f9a9e9ca9b15 h1:akZNhly7HjVQW5WHMwt/Y4C2m5DeWJCo7+qlmlnUNpw= +github.com/inngest/inngest v1.3.1-0.20241122155305-f9a9e9ca9b15/go.mod h1:WtTSpwUMRNc5ZbuPUSlHWF0GI0ntQ2YJ438LJEzxFkc= github.com/karlseguin/ccache/v2 v2.0.8 h1:lT38cE//uyf6KcFok0rlgXtGFBWxkI6h/qg4tbFyDnA= github.com/karlseguin/ccache/v2 v2.0.8/go.mod h1:2BDThcfQMf/c0jnZowt16eW405XIqZPavt+HoYEtcxQ= github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003 h1:vJ0Snvo+SLMY72r5J4sEfkuE7AFbixEP2qRbEcum/wA= @@ -78,6 +90,8 @@ github.com/ohler55/ojg v1.24.1 h1:PaVLelrNgT5/0ppPaUtey54tOVp245z33fkhL2jljjY= github.com/ohler55/ojg v1.24.1/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0= +github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= @@ -104,6 +118,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sashabaranov/go-openai v1.35.6 h1:oi0rwCvyxMxgFALDGnyqFTyCJm6n72OnEG3sybIFR0g= github.com/sashabaranov/go-openai v1.35.6/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -137,10 +153,16 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= +github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/vektah/gqlparser/v2 v2.5.15 h1:fYdnU8roQniJziV5TDiFPm/Ff7pE8xbVSOJqbsdl88A= +github.com/vektah/gqlparser/v2 v2.5.15/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= @@ -183,6 +205,8 @@ golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -213,6 +237,8 @@ golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/golang.org/x/exp/slog/attr.go b/vendor/golang.org/x/exp/slog/attr.go deleted file mode 100644 index a180d0e1..00000000 --- a/vendor/golang.org/x/exp/slog/attr.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slog - -import ( - "fmt" - "time" -) - -// An Attr is a key-value pair. -type Attr struct { - Key string - Value Value -} - -// String returns an Attr for a string value. -func String(key, value string) Attr { - return Attr{key, StringValue(value)} -} - -// Int64 returns an Attr for an int64. -func Int64(key string, value int64) Attr { - return Attr{key, Int64Value(value)} -} - -// Int converts an int to an int64 and returns -// an Attr with that value. -func Int(key string, value int) Attr { - return Int64(key, int64(value)) -} - -// Uint64 returns an Attr for a uint64. -func Uint64(key string, v uint64) Attr { - return Attr{key, Uint64Value(v)} -} - -// Float64 returns an Attr for a floating-point number. -func Float64(key string, v float64) Attr { - return Attr{key, Float64Value(v)} -} - -// Bool returns an Attr for a bool. -func Bool(key string, v bool) Attr { - return Attr{key, BoolValue(v)} -} - -// Time returns an Attr for a time.Time. -// It discards the monotonic portion. -func Time(key string, v time.Time) Attr { - return Attr{key, TimeValue(v)} -} - -// Duration returns an Attr for a time.Duration. -func Duration(key string, v time.Duration) Attr { - return Attr{key, DurationValue(v)} -} - -// Group returns an Attr for a Group Value. -// The first argument is the key; the remaining arguments -// are converted to Attrs as in [Logger.Log]. -// -// Use Group to collect several key-value pairs under a single -// key on a log line, or as the result of LogValue -// in order to log a single value as multiple Attrs. -func Group(key string, args ...any) Attr { - return Attr{key, GroupValue(argsToAttrSlice(args)...)} -} - -func argsToAttrSlice(args []any) []Attr { - var ( - attr Attr - attrs []Attr - ) - for len(args) > 0 { - attr, args = argsToAttr(args) - attrs = append(attrs, attr) - } - return attrs -} - -// Any returns an Attr for the supplied value. -// See [Value.AnyValue] for how values are treated. -func Any(key string, value any) Attr { - return Attr{key, AnyValue(value)} -} - -// Equal reports whether a and b have equal keys and values. -func (a Attr) Equal(b Attr) bool { - return a.Key == b.Key && a.Value.Equal(b.Value) -} - -func (a Attr) String() string { - return fmt.Sprintf("%s=%s", a.Key, a.Value) -} - -// isEmpty reports whether a has an empty key and a nil value. -// That can be written as Attr{} or Any("", nil). -func (a Attr) isEmpty() bool { - return a.Key == "" && a.Value.num == 0 && a.Value.any == nil -} diff --git a/vendor/golang.org/x/exp/slog/doc.go b/vendor/golang.org/x/exp/slog/doc.go deleted file mode 100644 index 4beaf867..00000000 --- a/vendor/golang.org/x/exp/slog/doc.go +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package slog provides structured logging, -in which log records include a message, -a severity level, and various other attributes -expressed as key-value pairs. - -It defines a type, [Logger], -which provides several methods (such as [Logger.Info] and [Logger.Error]) -for reporting events of interest. - -Each Logger is associated with a [Handler]. -A Logger output method creates a [Record] from the method arguments -and passes it to the Handler, which decides how to handle it. -There is a default Logger accessible through top-level functions -(such as [Info] and [Error]) that call the corresponding Logger methods. - -A log record consists of a time, a level, a message, and a set of key-value -pairs, where the keys are strings and the values may be of any type. -As an example, - - slog.Info("hello", "count", 3) - -creates a record containing the time of the call, -a level of Info, the message "hello", and a single -pair with key "count" and value 3. - -The [Info] top-level function calls the [Logger.Info] method on the default Logger. -In addition to [Logger.Info], there are methods for Debug, Warn and Error levels. -Besides these convenience methods for common levels, -there is also a [Logger.Log] method which takes the level as an argument. -Each of these methods has a corresponding top-level function that uses the -default logger. - -The default handler formats the log record's message, time, level, and attributes -as a string and passes it to the [log] package. - - 2022/11/08 15:28:26 INFO hello count=3 - -For more control over the output format, create a logger with a different handler. -This statement uses [New] to create a new logger with a TextHandler -that writes structured records in text form to standard error: - - logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) - -[TextHandler] output is a sequence of key=value pairs, easily and unambiguously -parsed by machine. This statement: - - logger.Info("hello", "count", 3) - -produces this output: - - time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3 - -The package also provides [JSONHandler], whose output is line-delimited JSON: - - logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - logger.Info("hello", "count", 3) - -produces this output: - - {"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3} - -Both [TextHandler] and [JSONHandler] can be configured with [HandlerOptions]. -There are options for setting the minimum level (see Levels, below), -displaying the source file and line of the log call, and -modifying attributes before they are logged. - -Setting a logger as the default with - - slog.SetDefault(logger) - -will cause the top-level functions like [Info] to use it. -[SetDefault] also updates the default logger used by the [log] package, -so that existing applications that use [log.Printf] and related functions -will send log records to the logger's handler without needing to be rewritten. - -Some attributes are common to many log calls. -For example, you may wish to include the URL or trace identifier of a server request -with all log events arising from the request. -Rather than repeat the attribute with every log call, you can use [Logger.With] -to construct a new Logger containing the attributes: - - logger2 := logger.With("url", r.URL) - -The arguments to With are the same key-value pairs used in [Logger.Info]. -The result is a new Logger with the same handler as the original, but additional -attributes that will appear in the output of every call. - -# Levels - -A [Level] is an integer representing the importance or severity of a log event. -The higher the level, the more severe the event. -This package defines constants for the most common levels, -but any int can be used as a level. - -In an application, you may wish to log messages only at a certain level or greater. -One common configuration is to log messages at Info or higher levels, -suppressing debug logging until it is needed. -The built-in handlers can be configured with the minimum level to output by -setting [HandlerOptions.Level]. -The program's `main` function typically does this. -The default value is LevelInfo. - -Setting the [HandlerOptions.Level] field to a [Level] value -fixes the handler's minimum level throughout its lifetime. -Setting it to a [LevelVar] allows the level to be varied dynamically. -A LevelVar holds a Level and is safe to read or write from multiple -goroutines. -To vary the level dynamically for an entire program, first initialize -a global LevelVar: - - var programLevel = new(slog.LevelVar) // Info by default - -Then use the LevelVar to construct a handler, and make it the default: - - h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}) - slog.SetDefault(slog.New(h)) - -Now the program can change its logging level with a single statement: - - programLevel.Set(slog.LevelDebug) - -# Groups - -Attributes can be collected into groups. -A group has a name that is used to qualify the names of its attributes. -How this qualification is displayed depends on the handler. -[TextHandler] separates the group and attribute names with a dot. -[JSONHandler] treats each group as a separate JSON object, with the group name as the key. - -Use [Group] to create a Group attribute from a name and a list of key-value pairs: - - slog.Group("request", - "method", r.Method, - "url", r.URL) - -TextHandler would display this group as - - request.method=GET request.url=http://example.com - -JSONHandler would display it as - - "request":{"method":"GET","url":"http://example.com"} - -Use [Logger.WithGroup] to qualify all of a Logger's output -with a group name. Calling WithGroup on a Logger results in a -new Logger with the same Handler as the original, but with all -its attributes qualified by the group name. - -This can help prevent duplicate attribute keys in large systems, -where subsystems might use the same keys. -Pass each subsystem a different Logger with its own group name so that -potential duplicates are qualified: - - logger := slog.Default().With("id", systemID) - parserLogger := logger.WithGroup("parser") - parseInput(input, parserLogger) - -When parseInput logs with parserLogger, its keys will be qualified with "parser", -so even if it uses the common key "id", the log line will have distinct keys. - -# Contexts - -Some handlers may wish to include information from the [context.Context] that is -available at the call site. One example of such information -is the identifier for the current span when tracing is enabled. - -The [Logger.Log] and [Logger.LogAttrs] methods take a context as a first -argument, as do their corresponding top-level functions. - -Although the convenience methods on Logger (Info and so on) and the -corresponding top-level functions do not take a context, the alternatives ending -in "Context" do. For example, - - slog.InfoContext(ctx, "message") - -It is recommended to pass a context to an output method if one is available. - -# Attrs and Values - -An [Attr] is a key-value pair. The Logger output methods accept Attrs as well as -alternating keys and values. The statement - - slog.Info("hello", slog.Int("count", 3)) - -behaves the same as - - slog.Info("hello", "count", 3) - -There are convenience constructors for [Attr] such as [Int], [String], and [Bool] -for common types, as well as the function [Any] for constructing Attrs of any -type. - -The value part of an Attr is a type called [Value]. -Like an [any], a Value can hold any Go value, -but it can represent typical values, including all numbers and strings, -without an allocation. - -For the most efficient log output, use [Logger.LogAttrs]. -It is similar to [Logger.Log] but accepts only Attrs, not alternating -keys and values; this allows it, too, to avoid allocation. - -The call - - logger.LogAttrs(nil, slog.LevelInfo, "hello", slog.Int("count", 3)) - -is the most efficient way to achieve the same output as - - slog.Info("hello", "count", 3) - -# Customizing a type's logging behavior - -If a type implements the [LogValuer] interface, the [Value] returned from its LogValue -method is used for logging. You can use this to control how values of the type -appear in logs. For example, you can redact secret information like passwords, -or gather a struct's fields in a Group. See the examples under [LogValuer] for -details. - -A LogValue method may return a Value that itself implements [LogValuer]. The [Value.Resolve] -method handles these cases carefully, avoiding infinite loops and unbounded recursion. -Handler authors and others may wish to use Value.Resolve instead of calling LogValue directly. - -# Wrapping output methods - -The logger functions use reflection over the call stack to find the file name -and line number of the logging call within the application. This can produce -incorrect source information for functions that wrap slog. For instance, if you -define this function in file mylog.go: - - func Infof(format string, args ...any) { - slog.Default().Info(fmt.Sprintf(format, args...)) - } - -and you call it like this in main.go: - - Infof(slog.Default(), "hello, %s", "world") - -then slog will report the source file as mylog.go, not main.go. - -A correct implementation of Infof will obtain the source location -(pc) and pass it to NewRecord. -The Infof function in the package-level example called "wrapping" -demonstrates how to do this. - -# Working with Records - -Sometimes a Handler will need to modify a Record -before passing it on to another Handler or backend. -A Record contains a mixture of simple public fields (e.g. Time, Level, Message) -and hidden fields that refer to state (such as attributes) indirectly. This -means that modifying a simple copy of a Record (e.g. by calling -[Record.Add] or [Record.AddAttrs] to add attributes) -may have unexpected effects on the original. -Before modifying a Record, use [Clone] to -create a copy that shares no state with the original, -or create a new Record with [NewRecord] -and build up its Attrs by traversing the old ones with [Record.Attrs]. - -# Performance considerations - -If profiling your application demonstrates that logging is taking significant time, -the following suggestions may help. - -If many log lines have a common attribute, use [Logger.With] to create a Logger with -that attribute. The built-in handlers will format that attribute only once, at the -call to [Logger.With]. The [Handler] interface is designed to allow that optimization, -and a well-written Handler should take advantage of it. - -The arguments to a log call are always evaluated, even if the log event is discarded. -If possible, defer computation so that it happens only if the value is actually logged. -For example, consider the call - - slog.Info("starting request", "url", r.URL.String()) // may compute String unnecessarily - -The URL.String method will be called even if the logger discards Info-level events. -Instead, pass the URL directly: - - slog.Info("starting request", "url", &r.URL) // calls URL.String only if needed - -The built-in [TextHandler] will call its String method, but only -if the log event is enabled. -Avoiding the call to String also preserves the structure of the underlying value. -For example [JSONHandler] emits the components of the parsed URL as a JSON object. -If you want to avoid eagerly paying the cost of the String call -without causing the handler to potentially inspect the structure of the value, -wrap the value in a fmt.Stringer implementation that hides its Marshal methods. - -You can also use the [LogValuer] interface to avoid unnecessary work in disabled log -calls. Say you need to log some expensive value: - - slog.Debug("frobbing", "value", computeExpensiveValue(arg)) - -Even if this line is disabled, computeExpensiveValue will be called. -To avoid that, define a type implementing LogValuer: - - type expensive struct { arg int } - - func (e expensive) LogValue() slog.Value { - return slog.AnyValue(computeExpensiveValue(e.arg)) - } - -Then use a value of that type in log calls: - - slog.Debug("frobbing", "value", expensive{arg}) - -Now computeExpensiveValue will only be called when the line is enabled. - -The built-in handlers acquire a lock before calling [io.Writer.Write] -to ensure that each record is written in one piece. User-defined -handlers are responsible for their own locking. -*/ -package slog diff --git a/vendor/golang.org/x/exp/slog/handler.go b/vendor/golang.org/x/exp/slog/handler.go deleted file mode 100644 index bd635cb8..00000000 --- a/vendor/golang.org/x/exp/slog/handler.go +++ /dev/null @@ -1,577 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slog - -import ( - "context" - "fmt" - "io" - "reflect" - "strconv" - "sync" - "time" - - "golang.org/x/exp/slices" - "golang.org/x/exp/slog/internal/buffer" -) - -// A Handler handles log records produced by a Logger.. -// -// A typical handler may print log records to standard error, -// or write them to a file or database, or perhaps augment them -// with additional attributes and pass them on to another handler. -// -// Any of the Handler's methods may be called concurrently with itself -// or with other methods. It is the responsibility of the Handler to -// manage this concurrency. -// -// Users of the slog package should not invoke Handler methods directly. -// They should use the methods of [Logger] instead. -type Handler interface { - // Enabled reports whether the handler handles records at the given level. - // The handler ignores records whose level is lower. - // It is called early, before any arguments are processed, - // to save effort if the log event should be discarded. - // If called from a Logger method, the first argument is the context - // passed to that method, or context.Background() if nil was passed - // or the method does not take a context. - // The context is passed so Enabled can use its values - // to make a decision. - Enabled(context.Context, Level) bool - - // Handle handles the Record. - // It will only be called when Enabled returns true. - // The Context argument is as for Enabled. - // It is present solely to provide Handlers access to the context's values. - // Canceling the context should not affect record processing. - // (Among other things, log messages may be necessary to debug a - // cancellation-related problem.) - // - // Handle methods that produce output should observe the following rules: - // - If r.Time is the zero time, ignore the time. - // - If r.PC is zero, ignore it. - // - Attr's values should be resolved. - // - If an Attr's key and value are both the zero value, ignore the Attr. - // This can be tested with attr.Equal(Attr{}). - // - If a group's key is empty, inline the group's Attrs. - // - If a group has no Attrs (even if it has a non-empty key), - // ignore it. - Handle(context.Context, Record) error - - // WithAttrs returns a new Handler whose attributes consist of - // both the receiver's attributes and the arguments. - // The Handler owns the slice: it may retain, modify or discard it. - WithAttrs(attrs []Attr) Handler - - // WithGroup returns a new Handler with the given group appended to - // the receiver's existing groups. - // The keys of all subsequent attributes, whether added by With or in a - // Record, should be qualified by the sequence of group names. - // - // How this qualification happens is up to the Handler, so long as - // this Handler's attribute keys differ from those of another Handler - // with a different sequence of group names. - // - // A Handler should treat WithGroup as starting a Group of Attrs that ends - // at the end of the log event. That is, - // - // logger.WithGroup("s").LogAttrs(level, msg, slog.Int("a", 1), slog.Int("b", 2)) - // - // should behave like - // - // logger.LogAttrs(level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2))) - // - // If the name is empty, WithGroup returns the receiver. - WithGroup(name string) Handler -} - -type defaultHandler struct { - ch *commonHandler - // log.Output, except for testing - output func(calldepth int, message string) error -} - -func newDefaultHandler(output func(int, string) error) *defaultHandler { - return &defaultHandler{ - ch: &commonHandler{json: false}, - output: output, - } -} - -func (*defaultHandler) Enabled(_ context.Context, l Level) bool { - return l >= LevelInfo -} - -// Collect the level, attributes and message in a string and -// write it with the default log.Logger. -// Let the log.Logger handle time and file/line. -func (h *defaultHandler) Handle(ctx context.Context, r Record) error { - buf := buffer.New() - buf.WriteString(r.Level.String()) - buf.WriteByte(' ') - buf.WriteString(r.Message) - state := h.ch.newHandleState(buf, true, " ", nil) - defer state.free() - state.appendNonBuiltIns(r) - - // skip [h.output, defaultHandler.Handle, handlerWriter.Write, log.Output] - return h.output(4, buf.String()) -} - -func (h *defaultHandler) WithAttrs(as []Attr) Handler { - return &defaultHandler{h.ch.withAttrs(as), h.output} -} - -func (h *defaultHandler) WithGroup(name string) Handler { - return &defaultHandler{h.ch.withGroup(name), h.output} -} - -// HandlerOptions are options for a TextHandler or JSONHandler. -// A zero HandlerOptions consists entirely of default values. -type HandlerOptions struct { - // AddSource causes the handler to compute the source code position - // of the log statement and add a SourceKey attribute to the output. - AddSource bool - - // Level reports the minimum record level that will be logged. - // The handler discards records with lower levels. - // If Level is nil, the handler assumes LevelInfo. - // The handler calls Level.Level for each record processed; - // to adjust the minimum level dynamically, use a LevelVar. - Level Leveler - - // ReplaceAttr is called to rewrite each non-group attribute before it is logged. - // The attribute's value has been resolved (see [Value.Resolve]). - // If ReplaceAttr returns an Attr with Key == "", the attribute is discarded. - // - // The built-in attributes with keys "time", "level", "source", and "msg" - // are passed to this function, except that time is omitted - // if zero, and source is omitted if AddSource is false. - // - // The first argument is a list of currently open groups that contain the - // Attr. It must not be retained or modified. ReplaceAttr is never called - // for Group attributes, only their contents. For example, the attribute - // list - // - // Int("a", 1), Group("g", Int("b", 2)), Int("c", 3) - // - // results in consecutive calls to ReplaceAttr with the following arguments: - // - // nil, Int("a", 1) - // []string{"g"}, Int("b", 2) - // nil, Int("c", 3) - // - // ReplaceAttr can be used to change the default keys of the built-in - // attributes, convert types (for example, to replace a `time.Time` with the - // integer seconds since the Unix epoch), sanitize personal information, or - // remove attributes from the output. - ReplaceAttr func(groups []string, a Attr) Attr -} - -// Keys for "built-in" attributes. -const ( - // TimeKey is the key used by the built-in handlers for the time - // when the log method is called. The associated Value is a [time.Time]. - TimeKey = "time" - // LevelKey is the key used by the built-in handlers for the level - // of the log call. The associated value is a [Level]. - LevelKey = "level" - // MessageKey is the key used by the built-in handlers for the - // message of the log call. The associated value is a string. - MessageKey = "msg" - // SourceKey is the key used by the built-in handlers for the source file - // and line of the log call. The associated value is a string. - SourceKey = "source" -) - -type commonHandler struct { - json bool // true => output JSON; false => output text - opts HandlerOptions - preformattedAttrs []byte - groupPrefix string // for text: prefix of groups opened in preformatting - groups []string // all groups started from WithGroup - nOpenGroups int // the number of groups opened in preformattedAttrs - mu sync.Mutex - w io.Writer -} - -func (h *commonHandler) clone() *commonHandler { - // We can't use assignment because we can't copy the mutex. - return &commonHandler{ - json: h.json, - opts: h.opts, - preformattedAttrs: slices.Clip(h.preformattedAttrs), - groupPrefix: h.groupPrefix, - groups: slices.Clip(h.groups), - nOpenGroups: h.nOpenGroups, - w: h.w, - } -} - -// enabled reports whether l is greater than or equal to the -// minimum level. -func (h *commonHandler) enabled(l Level) bool { - minLevel := LevelInfo - if h.opts.Level != nil { - minLevel = h.opts.Level.Level() - } - return l >= minLevel -} - -func (h *commonHandler) withAttrs(as []Attr) *commonHandler { - h2 := h.clone() - // Pre-format the attributes as an optimization. - prefix := buffer.New() - defer prefix.Free() - prefix.WriteString(h.groupPrefix) - state := h2.newHandleState((*buffer.Buffer)(&h2.preformattedAttrs), false, "", prefix) - defer state.free() - if len(h2.preformattedAttrs) > 0 { - state.sep = h.attrSep() - } - state.openGroups() - for _, a := range as { - state.appendAttr(a) - } - // Remember the new prefix for later keys. - h2.groupPrefix = state.prefix.String() - // Remember how many opened groups are in preformattedAttrs, - // so we don't open them again when we handle a Record. - h2.nOpenGroups = len(h2.groups) - return h2 -} - -func (h *commonHandler) withGroup(name string) *commonHandler { - if name == "" { - return h - } - h2 := h.clone() - h2.groups = append(h2.groups, name) - return h2 -} - -func (h *commonHandler) handle(r Record) error { - state := h.newHandleState(buffer.New(), true, "", nil) - defer state.free() - if h.json { - state.buf.WriteByte('{') - } - // Built-in attributes. They are not in a group. - stateGroups := state.groups - state.groups = nil // So ReplaceAttrs sees no groups instead of the pre groups. - rep := h.opts.ReplaceAttr - // time - if !r.Time.IsZero() { - key := TimeKey - val := r.Time.Round(0) // strip monotonic to match Attr behavior - if rep == nil { - state.appendKey(key) - state.appendTime(val) - } else { - state.appendAttr(Time(key, val)) - } - } - // level - key := LevelKey - val := r.Level - if rep == nil { - state.appendKey(key) - state.appendString(val.String()) - } else { - state.appendAttr(Any(key, val)) - } - // source - if h.opts.AddSource { - state.appendAttr(Any(SourceKey, r.source())) - } - key = MessageKey - msg := r.Message - if rep == nil { - state.appendKey(key) - state.appendString(msg) - } else { - state.appendAttr(String(key, msg)) - } - state.groups = stateGroups // Restore groups passed to ReplaceAttrs. - state.appendNonBuiltIns(r) - state.buf.WriteByte('\n') - - h.mu.Lock() - defer h.mu.Unlock() - _, err := h.w.Write(*state.buf) - return err -} - -func (s *handleState) appendNonBuiltIns(r Record) { - // preformatted Attrs - if len(s.h.preformattedAttrs) > 0 { - s.buf.WriteString(s.sep) - s.buf.Write(s.h.preformattedAttrs) - s.sep = s.h.attrSep() - } - // Attrs in Record -- unlike the built-in ones, they are in groups started - // from WithGroup. - s.prefix = buffer.New() - defer s.prefix.Free() - s.prefix.WriteString(s.h.groupPrefix) - s.openGroups() - r.Attrs(func(a Attr) bool { - s.appendAttr(a) - return true - }) - if s.h.json { - // Close all open groups. - for range s.h.groups { - s.buf.WriteByte('}') - } - // Close the top-level object. - s.buf.WriteByte('}') - } -} - -// attrSep returns the separator between attributes. -func (h *commonHandler) attrSep() string { - if h.json { - return "," - } - return " " -} - -// handleState holds state for a single call to commonHandler.handle. -// The initial value of sep determines whether to emit a separator -// before the next key, after which it stays true. -type handleState struct { - h *commonHandler - buf *buffer.Buffer - freeBuf bool // should buf be freed? - sep string // separator to write before next key - prefix *buffer.Buffer // for text: key prefix - groups *[]string // pool-allocated slice of active groups, for ReplaceAttr -} - -var groupPool = sync.Pool{New: func() any { - s := make([]string, 0, 10) - return &s -}} - -func (h *commonHandler) newHandleState(buf *buffer.Buffer, freeBuf bool, sep string, prefix *buffer.Buffer) handleState { - s := handleState{ - h: h, - buf: buf, - freeBuf: freeBuf, - sep: sep, - prefix: prefix, - } - if h.opts.ReplaceAttr != nil { - s.groups = groupPool.Get().(*[]string) - *s.groups = append(*s.groups, h.groups[:h.nOpenGroups]...) - } - return s -} - -func (s *handleState) free() { - if s.freeBuf { - s.buf.Free() - } - if gs := s.groups; gs != nil { - *gs = (*gs)[:0] - groupPool.Put(gs) - } -} - -func (s *handleState) openGroups() { - for _, n := range s.h.groups[s.h.nOpenGroups:] { - s.openGroup(n) - } -} - -// Separator for group names and keys. -const keyComponentSep = '.' - -// openGroup starts a new group of attributes -// with the given name. -func (s *handleState) openGroup(name string) { - if s.h.json { - s.appendKey(name) - s.buf.WriteByte('{') - s.sep = "" - } else { - s.prefix.WriteString(name) - s.prefix.WriteByte(keyComponentSep) - } - // Collect group names for ReplaceAttr. - if s.groups != nil { - *s.groups = append(*s.groups, name) - } -} - -// closeGroup ends the group with the given name. -func (s *handleState) closeGroup(name string) { - if s.h.json { - s.buf.WriteByte('}') - } else { - (*s.prefix) = (*s.prefix)[:len(*s.prefix)-len(name)-1 /* for keyComponentSep */] - } - s.sep = s.h.attrSep() - if s.groups != nil { - *s.groups = (*s.groups)[:len(*s.groups)-1] - } -} - -// appendAttr appends the Attr's key and value using app. -// It handles replacement and checking for an empty key. -// after replacement). -func (s *handleState) appendAttr(a Attr) { - if rep := s.h.opts.ReplaceAttr; rep != nil && a.Value.Kind() != KindGroup { - var gs []string - if s.groups != nil { - gs = *s.groups - } - // Resolve before calling ReplaceAttr, so the user doesn't have to. - a.Value = a.Value.Resolve() - a = rep(gs, a) - } - a.Value = a.Value.Resolve() - // Elide empty Attrs. - if a.isEmpty() { - return - } - // Special case: Source. - if v := a.Value; v.Kind() == KindAny { - if src, ok := v.Any().(*Source); ok { - if s.h.json { - a.Value = src.group() - } else { - a.Value = StringValue(fmt.Sprintf("%s:%d", src.File, src.Line)) - } - } - } - if a.Value.Kind() == KindGroup { - attrs := a.Value.Group() - // Output only non-empty groups. - if len(attrs) > 0 { - // Inline a group with an empty key. - if a.Key != "" { - s.openGroup(a.Key) - } - for _, aa := range attrs { - s.appendAttr(aa) - } - if a.Key != "" { - s.closeGroup(a.Key) - } - } - } else { - s.appendKey(a.Key) - s.appendValue(a.Value) - } -} - -func (s *handleState) appendError(err error) { - s.appendString(fmt.Sprintf("!ERROR:%v", err)) -} - -func (s *handleState) appendKey(key string) { - s.buf.WriteString(s.sep) - if s.prefix != nil { - // TODO: optimize by avoiding allocation. - s.appendString(string(*s.prefix) + key) - } else { - s.appendString(key) - } - if s.h.json { - s.buf.WriteByte(':') - } else { - s.buf.WriteByte('=') - } - s.sep = s.h.attrSep() -} - -func (s *handleState) appendString(str string) { - if s.h.json { - s.buf.WriteByte('"') - *s.buf = appendEscapedJSONString(*s.buf, str) - s.buf.WriteByte('"') - } else { - // text - if needsQuoting(str) { - *s.buf = strconv.AppendQuote(*s.buf, str) - } else { - s.buf.WriteString(str) - } - } -} - -func (s *handleState) appendValue(v Value) { - defer func() { - if r := recover(); r != nil { - // If it panics with a nil pointer, the most likely cases are - // an encoding.TextMarshaler or error fails to guard against nil, - // in which case "" seems to be the feasible choice. - // - // Adapted from the code in fmt/print.go. - if v := reflect.ValueOf(v.any); v.Kind() == reflect.Pointer && v.IsNil() { - s.appendString("") - return - } - - // Otherwise just print the original panic message. - s.appendString(fmt.Sprintf("!PANIC: %v", r)) - } - }() - - var err error - if s.h.json { - err = appendJSONValue(s, v) - } else { - err = appendTextValue(s, v) - } - if err != nil { - s.appendError(err) - } -} - -func (s *handleState) appendTime(t time.Time) { - if s.h.json { - appendJSONTime(s, t) - } else { - writeTimeRFC3339Millis(s.buf, t) - } -} - -// This takes half the time of Time.AppendFormat. -func writeTimeRFC3339Millis(buf *buffer.Buffer, t time.Time) { - year, month, day := t.Date() - buf.WritePosIntWidth(year, 4) - buf.WriteByte('-') - buf.WritePosIntWidth(int(month), 2) - buf.WriteByte('-') - buf.WritePosIntWidth(day, 2) - buf.WriteByte('T') - hour, min, sec := t.Clock() - buf.WritePosIntWidth(hour, 2) - buf.WriteByte(':') - buf.WritePosIntWidth(min, 2) - buf.WriteByte(':') - buf.WritePosIntWidth(sec, 2) - ns := t.Nanosecond() - buf.WriteByte('.') - buf.WritePosIntWidth(ns/1e6, 3) - _, offsetSeconds := t.Zone() - if offsetSeconds == 0 { - buf.WriteByte('Z') - } else { - offsetMinutes := offsetSeconds / 60 - if offsetMinutes < 0 { - buf.WriteByte('-') - offsetMinutes = -offsetMinutes - } else { - buf.WriteByte('+') - } - buf.WritePosIntWidth(offsetMinutes/60, 2) - buf.WriteByte(':') - buf.WritePosIntWidth(offsetMinutes%60, 2) - } -} diff --git a/vendor/golang.org/x/exp/slog/internal/buffer/buffer.go b/vendor/golang.org/x/exp/slog/internal/buffer/buffer.go deleted file mode 100644 index 7786c166..00000000 --- a/vendor/golang.org/x/exp/slog/internal/buffer/buffer.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package buffer provides a pool-allocated byte buffer. -package buffer - -import ( - "sync" -) - -// Buffer adapted from go/src/fmt/print.go -type Buffer []byte - -// Having an initial size gives a dramatic speedup. -var bufPool = sync.Pool{ - New: func() any { - b := make([]byte, 0, 1024) - return (*Buffer)(&b) - }, -} - -func New() *Buffer { - return bufPool.Get().(*Buffer) -} - -func (b *Buffer) Free() { - // To reduce peak allocation, return only smaller buffers to the pool. - const maxBufferSize = 16 << 10 - if cap(*b) <= maxBufferSize { - *b = (*b)[:0] - bufPool.Put(b) - } -} - -func (b *Buffer) Reset() { - *b = (*b)[:0] -} - -func (b *Buffer) Write(p []byte) (int, error) { - *b = append(*b, p...) - return len(p), nil -} - -func (b *Buffer) WriteString(s string) { - *b = append(*b, s...) -} - -func (b *Buffer) WriteByte(c byte) { - *b = append(*b, c) -} - -func (b *Buffer) WritePosInt(i int) { - b.WritePosIntWidth(i, 0) -} - -// WritePosIntWidth writes non-negative integer i to the buffer, padded on the left -// by zeroes to the given width. Use a width of 0 to omit padding. -func (b *Buffer) WritePosIntWidth(i, width int) { - // Cheap integer to fixed-width decimal ASCII. - // Copied from log/log.go. - - if i < 0 { - panic("negative int") - } - - // Assemble decimal in reverse order. - var bb [20]byte - bp := len(bb) - 1 - for i >= 10 || width > 1 { - width-- - q := i / 10 - bb[bp] = byte('0' + i - q*10) - bp-- - i = q - } - // i < 10 - bb[bp] = byte('0' + i) - b.Write(bb[bp:]) -} - -func (b *Buffer) String() string { - return string(*b) -} diff --git a/vendor/golang.org/x/exp/slog/internal/ignorepc.go b/vendor/golang.org/x/exp/slog/internal/ignorepc.go deleted file mode 100644 index d1256426..00000000 --- a/vendor/golang.org/x/exp/slog/internal/ignorepc.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package internal - -// If IgnorePC is true, do not invoke runtime.Callers to get the pc. -// This is solely for benchmarking the slowdown from runtime.Callers. -var IgnorePC = false diff --git a/vendor/golang.org/x/exp/slog/json_handler.go b/vendor/golang.org/x/exp/slog/json_handler.go deleted file mode 100644 index 157ada86..00000000 --- a/vendor/golang.org/x/exp/slog/json_handler.go +++ /dev/null @@ -1,336 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slog - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "strconv" - "time" - "unicode/utf8" - - "golang.org/x/exp/slog/internal/buffer" -) - -// JSONHandler is a Handler that writes Records to an io.Writer as -// line-delimited JSON objects. -type JSONHandler struct { - *commonHandler -} - -// NewJSONHandler creates a JSONHandler that writes to w, -// using the given options. -// If opts is nil, the default options are used. -func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler { - if opts == nil { - opts = &HandlerOptions{} - } - return &JSONHandler{ - &commonHandler{ - json: true, - w: w, - opts: *opts, - }, - } -} - -// Enabled reports whether the handler handles records at the given level. -// The handler ignores records whose level is lower. -func (h *JSONHandler) Enabled(_ context.Context, level Level) bool { - return h.commonHandler.enabled(level) -} - -// WithAttrs returns a new JSONHandler whose attributes consists -// of h's attributes followed by attrs. -func (h *JSONHandler) WithAttrs(attrs []Attr) Handler { - return &JSONHandler{commonHandler: h.commonHandler.withAttrs(attrs)} -} - -func (h *JSONHandler) WithGroup(name string) Handler { - return &JSONHandler{commonHandler: h.commonHandler.withGroup(name)} -} - -// Handle formats its argument Record as a JSON object on a single line. -// -// If the Record's time is zero, the time is omitted. -// Otherwise, the key is "time" -// and the value is output as with json.Marshal. -// -// If the Record's level is zero, the level is omitted. -// Otherwise, the key is "level" -// and the value of [Level.String] is output. -// -// If the AddSource option is set and source information is available, -// the key is "source" -// and the value is output as "FILE:LINE". -// -// The message's key is "msg". -// -// To modify these or other attributes, or remove them from the output, use -// [HandlerOptions.ReplaceAttr]. -// -// Values are formatted as with an [encoding/json.Encoder] with SetEscapeHTML(false), -// with two exceptions. -// -// First, an Attr whose Value is of type error is formatted as a string, by -// calling its Error method. Only errors in Attrs receive this special treatment, -// not errors embedded in structs, slices, maps or other data structures that -// are processed by the encoding/json package. -// -// Second, an encoding failure does not cause Handle to return an error. -// Instead, the error message is formatted as a string. -// -// Each call to Handle results in a single serialized call to io.Writer.Write. -func (h *JSONHandler) Handle(_ context.Context, r Record) error { - return h.commonHandler.handle(r) -} - -// Adapted from time.Time.MarshalJSON to avoid allocation. -func appendJSONTime(s *handleState, t time.Time) { - if y := t.Year(); y < 0 || y >= 10000 { - // RFC 3339 is clear that years are 4 digits exactly. - // See golang.org/issue/4556#c15 for more discussion. - s.appendError(errors.New("time.Time year outside of range [0,9999]")) - } - s.buf.WriteByte('"') - *s.buf = t.AppendFormat(*s.buf, time.RFC3339Nano) - s.buf.WriteByte('"') -} - -func appendJSONValue(s *handleState, v Value) error { - switch v.Kind() { - case KindString: - s.appendString(v.str()) - case KindInt64: - *s.buf = strconv.AppendInt(*s.buf, v.Int64(), 10) - case KindUint64: - *s.buf = strconv.AppendUint(*s.buf, v.Uint64(), 10) - case KindFloat64: - // json.Marshal is funny about floats; it doesn't - // always match strconv.AppendFloat. So just call it. - // That's expensive, but floats are rare. - if err := appendJSONMarshal(s.buf, v.Float64()); err != nil { - return err - } - case KindBool: - *s.buf = strconv.AppendBool(*s.buf, v.Bool()) - case KindDuration: - // Do what json.Marshal does. - *s.buf = strconv.AppendInt(*s.buf, int64(v.Duration()), 10) - case KindTime: - s.appendTime(v.Time()) - case KindAny: - a := v.Any() - _, jm := a.(json.Marshaler) - if err, ok := a.(error); ok && !jm { - s.appendString(err.Error()) - } else { - return appendJSONMarshal(s.buf, a) - } - default: - panic(fmt.Sprintf("bad kind: %s", v.Kind())) - } - return nil -} - -func appendJSONMarshal(buf *buffer.Buffer, v any) error { - // Use a json.Encoder to avoid escaping HTML. - var bb bytes.Buffer - enc := json.NewEncoder(&bb) - enc.SetEscapeHTML(false) - if err := enc.Encode(v); err != nil { - return err - } - bs := bb.Bytes() - buf.Write(bs[:len(bs)-1]) // remove final newline - return nil -} - -// appendEscapedJSONString escapes s for JSON and appends it to buf. -// It does not surround the string in quotation marks. -// -// Modified from encoding/json/encode.go:encodeState.string, -// with escapeHTML set to false. -func appendEscapedJSONString(buf []byte, s string) []byte { - char := func(b byte) { buf = append(buf, b) } - str := func(s string) { buf = append(buf, s...) } - - start := 0 - for i := 0; i < len(s); { - if b := s[i]; b < utf8.RuneSelf { - if safeSet[b] { - i++ - continue - } - if start < i { - str(s[start:i]) - } - char('\\') - switch b { - case '\\', '"': - char(b) - case '\n': - char('n') - case '\r': - char('r') - case '\t': - char('t') - default: - // This encodes bytes < 0x20 except for \t, \n and \r. - str(`u00`) - char(hex[b>>4]) - char(hex[b&0xF]) - } - i++ - start = i - continue - } - c, size := utf8.DecodeRuneInString(s[i:]) - if c == utf8.RuneError && size == 1 { - if start < i { - str(s[start:i]) - } - str(`\ufffd`) - i += size - start = i - continue - } - // U+2028 is LINE SEPARATOR. - // U+2029 is PARAGRAPH SEPARATOR. - // They are both technically valid characters in JSON strings, - // but don't work in JSONP, which has to be evaluated as JavaScript, - // and can lead to security holes there. It is valid JSON to - // escape them, so we do so unconditionally. - // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. - if c == '\u2028' || c == '\u2029' { - if start < i { - str(s[start:i]) - } - str(`\u202`) - char(hex[c&0xF]) - i += size - start = i - continue - } - i += size - } - if start < len(s) { - str(s[start:]) - } - return buf -} - -var hex = "0123456789abcdef" - -// Copied from encoding/json/tables.go. -// -// safeSet holds the value true if the ASCII character with the given array -// position can be represented inside a JSON string without any further -// escaping. -// -// All values are true except for the ASCII control characters (0-31), the -// double quote ("), and the backslash character ("\"). -var safeSet = [utf8.RuneSelf]bool{ - ' ': true, - '!': true, - '"': false, - '#': true, - '$': true, - '%': true, - '&': true, - '\'': true, - '(': true, - ')': true, - '*': true, - '+': true, - ',': true, - '-': true, - '.': true, - '/': true, - '0': true, - '1': true, - '2': true, - '3': true, - '4': true, - '5': true, - '6': true, - '7': true, - '8': true, - '9': true, - ':': true, - ';': true, - '<': true, - '=': true, - '>': true, - '?': true, - '@': true, - 'A': true, - 'B': true, - 'C': true, - 'D': true, - 'E': true, - 'F': true, - 'G': true, - 'H': true, - 'I': true, - 'J': true, - 'K': true, - 'L': true, - 'M': true, - 'N': true, - 'O': true, - 'P': true, - 'Q': true, - 'R': true, - 'S': true, - 'T': true, - 'U': true, - 'V': true, - 'W': true, - 'X': true, - 'Y': true, - 'Z': true, - '[': true, - '\\': false, - ']': true, - '^': true, - '_': true, - '`': true, - 'a': true, - 'b': true, - 'c': true, - 'd': true, - 'e': true, - 'f': true, - 'g': true, - 'h': true, - 'i': true, - 'j': true, - 'k': true, - 'l': true, - 'm': true, - 'n': true, - 'o': true, - 'p': true, - 'q': true, - 'r': true, - 's': true, - 't': true, - 'u': true, - 'v': true, - 'w': true, - 'x': true, - 'y': true, - 'z': true, - '{': true, - '|': true, - '}': true, - '~': true, - '\u007f': true, -} diff --git a/vendor/golang.org/x/exp/slog/level.go b/vendor/golang.org/x/exp/slog/level.go deleted file mode 100644 index b2365f0a..00000000 --- a/vendor/golang.org/x/exp/slog/level.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slog - -import ( - "errors" - "fmt" - "strconv" - "strings" - "sync/atomic" -) - -// A Level is the importance or severity of a log event. -// The higher the level, the more important or severe the event. -type Level int - -// Level numbers are inherently arbitrary, -// but we picked them to satisfy three constraints. -// Any system can map them to another numbering scheme if it wishes. -// -// First, we wanted the default level to be Info, Since Levels are ints, Info is -// the default value for int, zero. -// - -// Second, we wanted to make it easy to use levels to specify logger verbosity. -// Since a larger level means a more severe event, a logger that accepts events -// with smaller (or more negative) level means a more verbose logger. Logger -// verbosity is thus the negation of event severity, and the default verbosity -// of 0 accepts all events at least as severe as INFO. -// -// Third, we wanted some room between levels to accommodate schemes with named -// levels between ours. For example, Google Cloud Logging defines a Notice level -// between Info and Warn. Since there are only a few of these intermediate -// levels, the gap between the numbers need not be large. Our gap of 4 matches -// OpenTelemetry's mapping. Subtracting 9 from an OpenTelemetry level in the -// DEBUG, INFO, WARN and ERROR ranges converts it to the corresponding slog -// Level range. OpenTelemetry also has the names TRACE and FATAL, which slog -// does not. But those OpenTelemetry levels can still be represented as slog -// Levels by using the appropriate integers. -// -// Names for common levels. -const ( - LevelDebug Level = -4 - LevelInfo Level = 0 - LevelWarn Level = 4 - LevelError Level = 8 -) - -// String returns a name for the level. -// If the level has a name, then that name -// in uppercase is returned. -// If the level is between named values, then -// an integer is appended to the uppercased name. -// Examples: -// -// LevelWarn.String() => "WARN" -// (LevelInfo+2).String() => "INFO+2" -func (l Level) String() string { - str := func(base string, val Level) string { - if val == 0 { - return base - } - return fmt.Sprintf("%s%+d", base, val) - } - - switch { - case l < LevelInfo: - return str("DEBUG", l-LevelDebug) - case l < LevelWarn: - return str("INFO", l-LevelInfo) - case l < LevelError: - return str("WARN", l-LevelWarn) - default: - return str("ERROR", l-LevelError) - } -} - -// MarshalJSON implements [encoding/json.Marshaler] -// by quoting the output of [Level.String]. -func (l Level) MarshalJSON() ([]byte, error) { - // AppendQuote is sufficient for JSON-encoding all Level strings. - // They don't contain any runes that would produce invalid JSON - // when escaped. - return strconv.AppendQuote(nil, l.String()), nil -} - -// UnmarshalJSON implements [encoding/json.Unmarshaler] -// It accepts any string produced by [Level.MarshalJSON], -// ignoring case. -// It also accepts numeric offsets that would result in a different string on -// output. For example, "Error-8" would marshal as "INFO". -func (l *Level) UnmarshalJSON(data []byte) error { - s, err := strconv.Unquote(string(data)) - if err != nil { - return err - } - return l.parse(s) -} - -// MarshalText implements [encoding.TextMarshaler] -// by calling [Level.String]. -func (l Level) MarshalText() ([]byte, error) { - return []byte(l.String()), nil -} - -// UnmarshalText implements [encoding.TextUnmarshaler]. -// It accepts any string produced by [Level.MarshalText], -// ignoring case. -// It also accepts numeric offsets that would result in a different string on -// output. For example, "Error-8" would marshal as "INFO". -func (l *Level) UnmarshalText(data []byte) error { - return l.parse(string(data)) -} - -func (l *Level) parse(s string) (err error) { - defer func() { - if err != nil { - err = fmt.Errorf("slog: level string %q: %w", s, err) - } - }() - - name := s - offset := 0 - if i := strings.IndexAny(s, "+-"); i >= 0 { - name = s[:i] - offset, err = strconv.Atoi(s[i:]) - if err != nil { - return err - } - } - switch strings.ToUpper(name) { - case "DEBUG": - *l = LevelDebug - case "INFO": - *l = LevelInfo - case "WARN": - *l = LevelWarn - case "ERROR": - *l = LevelError - default: - return errors.New("unknown name") - } - *l += Level(offset) - return nil -} - -// Level returns the receiver. -// It implements Leveler. -func (l Level) Level() Level { return l } - -// A LevelVar is a Level variable, to allow a Handler level to change -// dynamically. -// It implements Leveler as well as a Set method, -// and it is safe for use by multiple goroutines. -// The zero LevelVar corresponds to LevelInfo. -type LevelVar struct { - val atomic.Int64 -} - -// Level returns v's level. -func (v *LevelVar) Level() Level { - return Level(int(v.val.Load())) -} - -// Set sets v's level to l. -func (v *LevelVar) Set(l Level) { - v.val.Store(int64(l)) -} - -func (v *LevelVar) String() string { - return fmt.Sprintf("LevelVar(%s)", v.Level()) -} - -// MarshalText implements [encoding.TextMarshaler] -// by calling [Level.MarshalText]. -func (v *LevelVar) MarshalText() ([]byte, error) { - return v.Level().MarshalText() -} - -// UnmarshalText implements [encoding.TextUnmarshaler] -// by calling [Level.UnmarshalText]. -func (v *LevelVar) UnmarshalText(data []byte) error { - var l Level - if err := l.UnmarshalText(data); err != nil { - return err - } - v.Set(l) - return nil -} - -// A Leveler provides a Level value. -// -// As Level itself implements Leveler, clients typically supply -// a Level value wherever a Leveler is needed, such as in HandlerOptions. -// Clients who need to vary the level dynamically can provide a more complex -// Leveler implementation such as *LevelVar. -type Leveler interface { - Level() Level -} diff --git a/vendor/golang.org/x/exp/slog/logger.go b/vendor/golang.org/x/exp/slog/logger.go deleted file mode 100644 index e87ec993..00000000 --- a/vendor/golang.org/x/exp/slog/logger.go +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slog - -import ( - "context" - "log" - "runtime" - "sync/atomic" - "time" - - "golang.org/x/exp/slog/internal" -) - -var defaultLogger atomic.Value - -func init() { - defaultLogger.Store(New(newDefaultHandler(log.Output))) -} - -// Default returns the default Logger. -func Default() *Logger { return defaultLogger.Load().(*Logger) } - -// SetDefault makes l the default Logger. -// After this call, output from the log package's default Logger -// (as with [log.Print], etc.) will be logged at LevelInfo using l's Handler. -func SetDefault(l *Logger) { - defaultLogger.Store(l) - // If the default's handler is a defaultHandler, then don't use a handleWriter, - // or we'll deadlock as they both try to acquire the log default mutex. - // The defaultHandler will use whatever the log default writer is currently - // set to, which is correct. - // This can occur with SetDefault(Default()). - // See TestSetDefault. - if _, ok := l.Handler().(*defaultHandler); !ok { - capturePC := log.Flags()&(log.Lshortfile|log.Llongfile) != 0 - log.SetOutput(&handlerWriter{l.Handler(), LevelInfo, capturePC}) - log.SetFlags(0) // we want just the log message, no time or location - } -} - -// handlerWriter is an io.Writer that calls a Handler. -// It is used to link the default log.Logger to the default slog.Logger. -type handlerWriter struct { - h Handler - level Level - capturePC bool -} - -func (w *handlerWriter) Write(buf []byte) (int, error) { - if !w.h.Enabled(context.Background(), w.level) { - return 0, nil - } - var pc uintptr - if !internal.IgnorePC && w.capturePC { - // skip [runtime.Callers, w.Write, Logger.Output, log.Print] - var pcs [1]uintptr - runtime.Callers(4, pcs[:]) - pc = pcs[0] - } - - // Remove final newline. - origLen := len(buf) // Report that the entire buf was written. - if len(buf) > 0 && buf[len(buf)-1] == '\n' { - buf = buf[:len(buf)-1] - } - r := NewRecord(time.Now(), w.level, string(buf), pc) - return origLen, w.h.Handle(context.Background(), r) -} - -// A Logger records structured information about each call to its -// Log, Debug, Info, Warn, and Error methods. -// For each call, it creates a Record and passes it to a Handler. -// -// To create a new Logger, call [New] or a Logger method -// that begins "With". -type Logger struct { - handler Handler // for structured logging -} - -func (l *Logger) clone() *Logger { - c := *l - return &c -} - -// Handler returns l's Handler. -func (l *Logger) Handler() Handler { return l.handler } - -// With returns a new Logger that includes the given arguments, converted to -// Attrs as in [Logger.Log]. -// The Attrs will be added to each output from the Logger. -// The new Logger shares the old Logger's context. -// The new Logger's handler is the result of calling WithAttrs on the receiver's -// handler. -func (l *Logger) With(args ...any) *Logger { - c := l.clone() - c.handler = l.handler.WithAttrs(argsToAttrSlice(args)) - return c -} - -// WithGroup returns a new Logger that starts a group. The keys of all -// attributes added to the Logger will be qualified by the given name. -// (How that qualification happens depends on the [Handler.WithGroup] -// method of the Logger's Handler.) -// The new Logger shares the old Logger's context. -// -// The new Logger's handler is the result of calling WithGroup on the receiver's -// handler. -func (l *Logger) WithGroup(name string) *Logger { - c := l.clone() - c.handler = l.handler.WithGroup(name) - return c - -} - -// New creates a new Logger with the given non-nil Handler and a nil context. -func New(h Handler) *Logger { - if h == nil { - panic("nil Handler") - } - return &Logger{handler: h} -} - -// With calls Logger.With on the default logger. -func With(args ...any) *Logger { - return Default().With(args...) -} - -// Enabled reports whether l emits log records at the given context and level. -func (l *Logger) Enabled(ctx context.Context, level Level) bool { - if ctx == nil { - ctx = context.Background() - } - return l.Handler().Enabled(ctx, level) -} - -// NewLogLogger returns a new log.Logger such that each call to its Output method -// dispatches a Record to the specified handler. The logger acts as a bridge from -// the older log API to newer structured logging handlers. -func NewLogLogger(h Handler, level Level) *log.Logger { - return log.New(&handlerWriter{h, level, true}, "", 0) -} - -// Log emits a log record with the current time and the given level and message. -// The Record's Attrs consist of the Logger's attributes followed by -// the Attrs specified by args. -// -// The attribute arguments are processed as follows: -// - If an argument is an Attr, it is used as is. -// - If an argument is a string and this is not the last argument, -// the following argument is treated as the value and the two are combined -// into an Attr. -// - Otherwise, the argument is treated as a value with key "!BADKEY". -func (l *Logger) Log(ctx context.Context, level Level, msg string, args ...any) { - l.log(ctx, level, msg, args...) -} - -// LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs. -func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) { - l.logAttrs(ctx, level, msg, attrs...) -} - -// Debug logs at LevelDebug. -func (l *Logger) Debug(msg string, args ...any) { - l.log(nil, LevelDebug, msg, args...) -} - -// DebugContext logs at LevelDebug with the given context. -func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any) { - l.log(ctx, LevelDebug, msg, args...) -} - -// DebugCtx logs at LevelDebug with the given context. -// Deprecated: Use Logger.DebugContext. -func (l *Logger) DebugCtx(ctx context.Context, msg string, args ...any) { - l.log(ctx, LevelDebug, msg, args...) -} - -// Info logs at LevelInfo. -func (l *Logger) Info(msg string, args ...any) { - l.log(nil, LevelInfo, msg, args...) -} - -// InfoContext logs at LevelInfo with the given context. -func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any) { - l.log(ctx, LevelInfo, msg, args...) -} - -// InfoCtx logs at LevelInfo with the given context. -// Deprecated: Use Logger.InfoContext. -func (l *Logger) InfoCtx(ctx context.Context, msg string, args ...any) { - l.log(ctx, LevelInfo, msg, args...) -} - -// Warn logs at LevelWarn. -func (l *Logger) Warn(msg string, args ...any) { - l.log(nil, LevelWarn, msg, args...) -} - -// WarnContext logs at LevelWarn with the given context. -func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any) { - l.log(ctx, LevelWarn, msg, args...) -} - -// WarnCtx logs at LevelWarn with the given context. -// Deprecated: Use Logger.WarnContext. -func (l *Logger) WarnCtx(ctx context.Context, msg string, args ...any) { - l.log(ctx, LevelWarn, msg, args...) -} - -// Error logs at LevelError. -func (l *Logger) Error(msg string, args ...any) { - l.log(nil, LevelError, msg, args...) -} - -// ErrorContext logs at LevelError with the given context. -func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any) { - l.log(ctx, LevelError, msg, args...) -} - -// ErrorCtx logs at LevelError with the given context. -// Deprecated: Use Logger.ErrorContext. -func (l *Logger) ErrorCtx(ctx context.Context, msg string, args ...any) { - l.log(ctx, LevelError, msg, args...) -} - -// log is the low-level logging method for methods that take ...any. -// It must always be called directly by an exported logging method -// or function, because it uses a fixed call depth to obtain the pc. -func (l *Logger) log(ctx context.Context, level Level, msg string, args ...any) { - if !l.Enabled(ctx, level) { - return - } - var pc uintptr - if !internal.IgnorePC { - var pcs [1]uintptr - // skip [runtime.Callers, this function, this function's caller] - runtime.Callers(3, pcs[:]) - pc = pcs[0] - } - r := NewRecord(time.Now(), level, msg, pc) - r.Add(args...) - if ctx == nil { - ctx = context.Background() - } - _ = l.Handler().Handle(ctx, r) -} - -// logAttrs is like [Logger.log], but for methods that take ...Attr. -func (l *Logger) logAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) { - if !l.Enabled(ctx, level) { - return - } - var pc uintptr - if !internal.IgnorePC { - var pcs [1]uintptr - // skip [runtime.Callers, this function, this function's caller] - runtime.Callers(3, pcs[:]) - pc = pcs[0] - } - r := NewRecord(time.Now(), level, msg, pc) - r.AddAttrs(attrs...) - if ctx == nil { - ctx = context.Background() - } - _ = l.Handler().Handle(ctx, r) -} - -// Debug calls Logger.Debug on the default logger. -func Debug(msg string, args ...any) { - Default().log(nil, LevelDebug, msg, args...) -} - -// DebugContext calls Logger.DebugContext on the default logger. -func DebugContext(ctx context.Context, msg string, args ...any) { - Default().log(ctx, LevelDebug, msg, args...) -} - -// Info calls Logger.Info on the default logger. -func Info(msg string, args ...any) { - Default().log(nil, LevelInfo, msg, args...) -} - -// InfoContext calls Logger.InfoContext on the default logger. -func InfoContext(ctx context.Context, msg string, args ...any) { - Default().log(ctx, LevelInfo, msg, args...) -} - -// Warn calls Logger.Warn on the default logger. -func Warn(msg string, args ...any) { - Default().log(nil, LevelWarn, msg, args...) -} - -// WarnContext calls Logger.WarnContext on the default logger. -func WarnContext(ctx context.Context, msg string, args ...any) { - Default().log(ctx, LevelWarn, msg, args...) -} - -// Error calls Logger.Error on the default logger. -func Error(msg string, args ...any) { - Default().log(nil, LevelError, msg, args...) -} - -// ErrorContext calls Logger.ErrorContext on the default logger. -func ErrorContext(ctx context.Context, msg string, args ...any) { - Default().log(ctx, LevelError, msg, args...) -} - -// DebugCtx calls Logger.DebugContext on the default logger. -// Deprecated: call DebugContext. -func DebugCtx(ctx context.Context, msg string, args ...any) { - Default().log(ctx, LevelDebug, msg, args...) -} - -// InfoCtx calls Logger.InfoContext on the default logger. -// Deprecated: call InfoContext. -func InfoCtx(ctx context.Context, msg string, args ...any) { - Default().log(ctx, LevelInfo, msg, args...) -} - -// WarnCtx calls Logger.WarnContext on the default logger. -// Deprecated: call WarnContext. -func WarnCtx(ctx context.Context, msg string, args ...any) { - Default().log(ctx, LevelWarn, msg, args...) -} - -// ErrorCtx calls Logger.ErrorContext on the default logger. -// Deprecated: call ErrorContext. -func ErrorCtx(ctx context.Context, msg string, args ...any) { - Default().log(ctx, LevelError, msg, args...) -} - -// Log calls Logger.Log on the default logger. -func Log(ctx context.Context, level Level, msg string, args ...any) { - Default().log(ctx, level, msg, args...) -} - -// LogAttrs calls Logger.LogAttrs on the default logger. -func LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) { - Default().logAttrs(ctx, level, msg, attrs...) -} diff --git a/vendor/golang.org/x/exp/slog/noplog.bench b/vendor/golang.org/x/exp/slog/noplog.bench deleted file mode 100644 index ed9296ff..00000000 --- a/vendor/golang.org/x/exp/slog/noplog.bench +++ /dev/null @@ -1,36 +0,0 @@ -goos: linux -goarch: amd64 -pkg: golang.org/x/exp/slog -cpu: Intel(R) Xeon(R) CPU @ 2.20GHz -BenchmarkNopLog/attrs-8 1000000 1090 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/attrs-8 1000000 1097 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/attrs-8 1000000 1078 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/attrs-8 1000000 1095 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/attrs-8 1000000 1096 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/attrs-parallel-8 4007268 308.2 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/attrs-parallel-8 4016138 299.7 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/attrs-parallel-8 4020529 305.9 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/attrs-parallel-8 3977829 303.4 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/attrs-parallel-8 3225438 318.5 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/keys-values-8 1179256 994.2 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/keys-values-8 1000000 1002 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/keys-values-8 1216710 993.2 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/keys-values-8 1000000 1013 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/keys-values-8 1000000 1016 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-8 989066 1163 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-8 994116 1163 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-8 1000000 1152 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-8 991675 1165 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-8 965268 1166 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-parallel-8 3955503 303.3 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-parallel-8 3861188 307.8 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-parallel-8 3967752 303.9 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-parallel-8 3955203 302.7 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/WithContext-parallel-8 3948278 301.1 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/Ctx-8 940622 1247 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/Ctx-8 936381 1257 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/Ctx-8 959730 1266 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/Ctx-8 943473 1290 ns/op 0 B/op 0 allocs/op -BenchmarkNopLog/Ctx-8 919414 1259 ns/op 0 B/op 0 allocs/op -PASS -ok golang.org/x/exp/slog 40.566s diff --git a/vendor/golang.org/x/exp/slog/record.go b/vendor/golang.org/x/exp/slog/record.go deleted file mode 100644 index 38b3440f..00000000 --- a/vendor/golang.org/x/exp/slog/record.go +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slog - -import ( - "runtime" - "time" - - "golang.org/x/exp/slices" -) - -const nAttrsInline = 5 - -// A Record holds information about a log event. -// Copies of a Record share state. -// Do not modify a Record after handing out a copy to it. -// Use [Record.Clone] to create a copy with no shared state. -type Record struct { - // The time at which the output method (Log, Info, etc.) was called. - Time time.Time - - // The log message. - Message string - - // The level of the event. - Level Level - - // The program counter at the time the record was constructed, as determined - // by runtime.Callers. If zero, no program counter is available. - // - // The only valid use for this value is as an argument to - // [runtime.CallersFrames]. In particular, it must not be passed to - // [runtime.FuncForPC]. - PC uintptr - - // Allocation optimization: an inline array sized to hold - // the majority of log calls (based on examination of open-source - // code). It holds the start of the list of Attrs. - front [nAttrsInline]Attr - - // The number of Attrs in front. - nFront int - - // The list of Attrs except for those in front. - // Invariants: - // - len(back) > 0 iff nFront == len(front) - // - Unused array elements are zero. Used to detect mistakes. - back []Attr -} - -// NewRecord creates a Record from the given arguments. -// Use [Record.AddAttrs] to add attributes to the Record. -// -// NewRecord is intended for logging APIs that want to support a [Handler] as -// a backend. -func NewRecord(t time.Time, level Level, msg string, pc uintptr) Record { - return Record{ - Time: t, - Message: msg, - Level: level, - PC: pc, - } -} - -// Clone returns a copy of the record with no shared state. -// The original record and the clone can both be modified -// without interfering with each other. -func (r Record) Clone() Record { - r.back = slices.Clip(r.back) // prevent append from mutating shared array - return r -} - -// NumAttrs returns the number of attributes in the Record. -func (r Record) NumAttrs() int { - return r.nFront + len(r.back) -} - -// Attrs calls f on each Attr in the Record. -// Iteration stops if f returns false. -func (r Record) Attrs(f func(Attr) bool) { - for i := 0; i < r.nFront; i++ { - if !f(r.front[i]) { - return - } - } - for _, a := range r.back { - if !f(a) { - return - } - } -} - -// AddAttrs appends the given Attrs to the Record's list of Attrs. -func (r *Record) AddAttrs(attrs ...Attr) { - n := copy(r.front[r.nFront:], attrs) - r.nFront += n - // Check if a copy was modified by slicing past the end - // and seeing if the Attr there is non-zero. - if cap(r.back) > len(r.back) { - end := r.back[:len(r.back)+1][len(r.back)] - if !end.isEmpty() { - panic("copies of a slog.Record were both modified") - } - } - r.back = append(r.back, attrs[n:]...) -} - -// Add converts the args to Attrs as described in [Logger.Log], -// then appends the Attrs to the Record's list of Attrs. -func (r *Record) Add(args ...any) { - var a Attr - for len(args) > 0 { - a, args = argsToAttr(args) - if r.nFront < len(r.front) { - r.front[r.nFront] = a - r.nFront++ - } else { - if r.back == nil { - r.back = make([]Attr, 0, countAttrs(args)) - } - r.back = append(r.back, a) - } - } - -} - -// countAttrs returns the number of Attrs that would be created from args. -func countAttrs(args []any) int { - n := 0 - for i := 0; i < len(args); i++ { - n++ - if _, ok := args[i].(string); ok { - i++ - } - } - return n -} - -const badKey = "!BADKEY" - -// argsToAttr turns a prefix of the nonempty args slice into an Attr -// and returns the unconsumed portion of the slice. -// If args[0] is an Attr, it returns it. -// If args[0] is a string, it treats the first two elements as -// a key-value pair. -// Otherwise, it treats args[0] as a value with a missing key. -func argsToAttr(args []any) (Attr, []any) { - switch x := args[0].(type) { - case string: - if len(args) == 1 { - return String(badKey, x), nil - } - return Any(x, args[1]), args[2:] - - case Attr: - return x, args[1:] - - default: - return Any(badKey, x), args[1:] - } -} - -// Source describes the location of a line of source code. -type Source struct { - // Function is the package path-qualified function name containing the - // source line. If non-empty, this string uniquely identifies a single - // function in the program. This may be the empty string if not known. - Function string `json:"function"` - // File and Line are the file name and line number (1-based) of the source - // line. These may be the empty string and zero, respectively, if not known. - File string `json:"file"` - Line int `json:"line"` -} - -// attrs returns the non-zero fields of s as a slice of attrs. -// It is similar to a LogValue method, but we don't want Source -// to implement LogValuer because it would be resolved before -// the ReplaceAttr function was called. -func (s *Source) group() Value { - var as []Attr - if s.Function != "" { - as = append(as, String("function", s.Function)) - } - if s.File != "" { - as = append(as, String("file", s.File)) - } - if s.Line != 0 { - as = append(as, Int("line", s.Line)) - } - return GroupValue(as...) -} - -// source returns a Source for the log event. -// If the Record was created without the necessary information, -// or if the location is unavailable, it returns a non-nil *Source -// with zero fields. -func (r Record) source() *Source { - fs := runtime.CallersFrames([]uintptr{r.PC}) - f, _ := fs.Next() - return &Source{ - Function: f.Function, - File: f.File, - Line: f.Line, - } -} diff --git a/vendor/golang.org/x/exp/slog/text_handler.go b/vendor/golang.org/x/exp/slog/text_handler.go deleted file mode 100644 index 75b66b71..00000000 --- a/vendor/golang.org/x/exp/slog/text_handler.go +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slog - -import ( - "context" - "encoding" - "fmt" - "io" - "reflect" - "strconv" - "unicode" - "unicode/utf8" -) - -// TextHandler is a Handler that writes Records to an io.Writer as a -// sequence of key=value pairs separated by spaces and followed by a newline. -type TextHandler struct { - *commonHandler -} - -// NewTextHandler creates a TextHandler that writes to w, -// using the given options. -// If opts is nil, the default options are used. -func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler { - if opts == nil { - opts = &HandlerOptions{} - } - return &TextHandler{ - &commonHandler{ - json: false, - w: w, - opts: *opts, - }, - } -} - -// Enabled reports whether the handler handles records at the given level. -// The handler ignores records whose level is lower. -func (h *TextHandler) Enabled(_ context.Context, level Level) bool { - return h.commonHandler.enabled(level) -} - -// WithAttrs returns a new TextHandler whose attributes consists -// of h's attributes followed by attrs. -func (h *TextHandler) WithAttrs(attrs []Attr) Handler { - return &TextHandler{commonHandler: h.commonHandler.withAttrs(attrs)} -} - -func (h *TextHandler) WithGroup(name string) Handler { - return &TextHandler{commonHandler: h.commonHandler.withGroup(name)} -} - -// Handle formats its argument Record as a single line of space-separated -// key=value items. -// -// If the Record's time is zero, the time is omitted. -// Otherwise, the key is "time" -// and the value is output in RFC3339 format with millisecond precision. -// -// If the Record's level is zero, the level is omitted. -// Otherwise, the key is "level" -// and the value of [Level.String] is output. -// -// If the AddSource option is set and source information is available, -// the key is "source" and the value is output as FILE:LINE. -// -// The message's key is "msg". -// -// To modify these or other attributes, or remove them from the output, use -// [HandlerOptions.ReplaceAttr]. -// -// If a value implements [encoding.TextMarshaler], the result of MarshalText is -// written. Otherwise, the result of fmt.Sprint is written. -// -// Keys and values are quoted with [strconv.Quote] if they contain Unicode space -// characters, non-printing characters, '"' or '='. -// -// Keys inside groups consist of components (keys or group names) separated by -// dots. No further escaping is performed. -// Thus there is no way to determine from the key "a.b.c" whether there -// are two groups "a" and "b" and a key "c", or a single group "a.b" and a key "c", -// or single group "a" and a key "b.c". -// If it is necessary to reconstruct the group structure of a key -// even in the presence of dots inside components, use -// [HandlerOptions.ReplaceAttr] to encode that information in the key. -// -// Each call to Handle results in a single serialized call to -// io.Writer.Write. -func (h *TextHandler) Handle(_ context.Context, r Record) error { - return h.commonHandler.handle(r) -} - -func appendTextValue(s *handleState, v Value) error { - switch v.Kind() { - case KindString: - s.appendString(v.str()) - case KindTime: - s.appendTime(v.time()) - case KindAny: - if tm, ok := v.any.(encoding.TextMarshaler); ok { - data, err := tm.MarshalText() - if err != nil { - return err - } - // TODO: avoid the conversion to string. - s.appendString(string(data)) - return nil - } - if bs, ok := byteSlice(v.any); ok { - // As of Go 1.19, this only allocates for strings longer than 32 bytes. - s.buf.WriteString(strconv.Quote(string(bs))) - return nil - } - s.appendString(fmt.Sprintf("%+v", v.Any())) - default: - *s.buf = v.append(*s.buf) - } - return nil -} - -// byteSlice returns its argument as a []byte if the argument's -// underlying type is []byte, along with a second return value of true. -// Otherwise it returns nil, false. -func byteSlice(a any) ([]byte, bool) { - if bs, ok := a.([]byte); ok { - return bs, true - } - // Like Printf's %s, we allow both the slice type and the byte element type to be named. - t := reflect.TypeOf(a) - if t != nil && t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 { - return reflect.ValueOf(a).Bytes(), true - } - return nil, false -} - -func needsQuoting(s string) bool { - if len(s) == 0 { - return true - } - for i := 0; i < len(s); { - b := s[i] - if b < utf8.RuneSelf { - // Quote anything except a backslash that would need quoting in a - // JSON string, as well as space and '=' - if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) { - return true - } - i++ - continue - } - r, size := utf8.DecodeRuneInString(s[i:]) - if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) { - return true - } - i += size - } - return false -} diff --git a/vendor/golang.org/x/exp/slog/value.go b/vendor/golang.org/x/exp/slog/value.go deleted file mode 100644 index 3550c46f..00000000 --- a/vendor/golang.org/x/exp/slog/value.go +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slog - -import ( - "fmt" - "math" - "runtime" - "strconv" - "strings" - "time" - "unsafe" - - "golang.org/x/exp/slices" -) - -// A Value can represent any Go value, but unlike type any, -// it can represent most small values without an allocation. -// The zero Value corresponds to nil. -type Value struct { - _ [0]func() // disallow == - // num holds the value for Kinds Int64, Uint64, Float64, Bool and Duration, - // the string length for KindString, and nanoseconds since the epoch for KindTime. - num uint64 - // If any is of type Kind, then the value is in num as described above. - // If any is of type *time.Location, then the Kind is Time and time.Time value - // can be constructed from the Unix nanos in num and the location (monotonic time - // is not preserved). - // If any is of type stringptr, then the Kind is String and the string value - // consists of the length in num and the pointer in any. - // Otherwise, the Kind is Any and any is the value. - // (This implies that Attrs cannot store values of type Kind, *time.Location - // or stringptr.) - any any -} - -// Kind is the kind of a Value. -type Kind int - -// The following list is sorted alphabetically, but it's also important that -// KindAny is 0 so that a zero Value represents nil. - -const ( - KindAny Kind = iota - KindBool - KindDuration - KindFloat64 - KindInt64 - KindString - KindTime - KindUint64 - KindGroup - KindLogValuer -) - -var kindStrings = []string{ - "Any", - "Bool", - "Duration", - "Float64", - "Int64", - "String", - "Time", - "Uint64", - "Group", - "LogValuer", -} - -func (k Kind) String() string { - if k >= 0 && int(k) < len(kindStrings) { - return kindStrings[k] - } - return "" -} - -// Unexported version of Kind, just so we can store Kinds in Values. -// (No user-provided value has this type.) -type kind Kind - -// Kind returns v's Kind. -func (v Value) Kind() Kind { - switch x := v.any.(type) { - case Kind: - return x - case stringptr: - return KindString - case timeLocation: - return KindTime - case groupptr: - return KindGroup - case LogValuer: - return KindLogValuer - case kind: // a kind is just a wrapper for a Kind - return KindAny - default: - return KindAny - } -} - -//////////////// Constructors - -// IntValue returns a Value for an int. -func IntValue(v int) Value { - return Int64Value(int64(v)) -} - -// Int64Value returns a Value for an int64. -func Int64Value(v int64) Value { - return Value{num: uint64(v), any: KindInt64} -} - -// Uint64Value returns a Value for a uint64. -func Uint64Value(v uint64) Value { - return Value{num: v, any: KindUint64} -} - -// Float64Value returns a Value for a floating-point number. -func Float64Value(v float64) Value { - return Value{num: math.Float64bits(v), any: KindFloat64} -} - -// BoolValue returns a Value for a bool. -func BoolValue(v bool) Value { - u := uint64(0) - if v { - u = 1 - } - return Value{num: u, any: KindBool} -} - -// Unexported version of *time.Location, just so we can store *time.Locations in -// Values. (No user-provided value has this type.) -type timeLocation *time.Location - -// TimeValue returns a Value for a time.Time. -// It discards the monotonic portion. -func TimeValue(v time.Time) Value { - if v.IsZero() { - // UnixNano on the zero time is undefined, so represent the zero time - // with a nil *time.Location instead. time.Time.Location method never - // returns nil, so a Value with any == timeLocation(nil) cannot be - // mistaken for any other Value, time.Time or otherwise. - return Value{any: timeLocation(nil)} - } - return Value{num: uint64(v.UnixNano()), any: timeLocation(v.Location())} -} - -// DurationValue returns a Value for a time.Duration. -func DurationValue(v time.Duration) Value { - return Value{num: uint64(v.Nanoseconds()), any: KindDuration} -} - -// AnyValue returns a Value for the supplied value. -// -// If the supplied value is of type Value, it is returned -// unmodified. -// -// Given a value of one of Go's predeclared string, bool, or -// (non-complex) numeric types, AnyValue returns a Value of kind -// String, Bool, Uint64, Int64, or Float64. The width of the -// original numeric type is not preserved. -// -// Given a time.Time or time.Duration value, AnyValue returns a Value of kind -// KindTime or KindDuration. The monotonic time is not preserved. -// -// For nil, or values of all other types, including named types whose -// underlying type is numeric, AnyValue returns a value of kind KindAny. -func AnyValue(v any) Value { - switch v := v.(type) { - case string: - return StringValue(v) - case int: - return Int64Value(int64(v)) - case uint: - return Uint64Value(uint64(v)) - case int64: - return Int64Value(v) - case uint64: - return Uint64Value(v) - case bool: - return BoolValue(v) - case time.Duration: - return DurationValue(v) - case time.Time: - return TimeValue(v) - case uint8: - return Uint64Value(uint64(v)) - case uint16: - return Uint64Value(uint64(v)) - case uint32: - return Uint64Value(uint64(v)) - case uintptr: - return Uint64Value(uint64(v)) - case int8: - return Int64Value(int64(v)) - case int16: - return Int64Value(int64(v)) - case int32: - return Int64Value(int64(v)) - case float64: - return Float64Value(v) - case float32: - return Float64Value(float64(v)) - case []Attr: - return GroupValue(v...) - case Kind: - return Value{any: kind(v)} - case Value: - return v - default: - return Value{any: v} - } -} - -//////////////// Accessors - -// Any returns v's value as an any. -func (v Value) Any() any { - switch v.Kind() { - case KindAny: - if k, ok := v.any.(kind); ok { - return Kind(k) - } - return v.any - case KindLogValuer: - return v.any - case KindGroup: - return v.group() - case KindInt64: - return int64(v.num) - case KindUint64: - return v.num - case KindFloat64: - return v.float() - case KindString: - return v.str() - case KindBool: - return v.bool() - case KindDuration: - return v.duration() - case KindTime: - return v.time() - default: - panic(fmt.Sprintf("bad kind: %s", v.Kind())) - } -} - -// Int64 returns v's value as an int64. It panics -// if v is not a signed integer. -func (v Value) Int64() int64 { - if g, w := v.Kind(), KindInt64; g != w { - panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) - } - return int64(v.num) -} - -// Uint64 returns v's value as a uint64. It panics -// if v is not an unsigned integer. -func (v Value) Uint64() uint64 { - if g, w := v.Kind(), KindUint64; g != w { - panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) - } - return v.num -} - -// Bool returns v's value as a bool. It panics -// if v is not a bool. -func (v Value) Bool() bool { - if g, w := v.Kind(), KindBool; g != w { - panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) - } - return v.bool() -} - -func (v Value) bool() bool { - return v.num == 1 -} - -// Duration returns v's value as a time.Duration. It panics -// if v is not a time.Duration. -func (v Value) Duration() time.Duration { - if g, w := v.Kind(), KindDuration; g != w { - panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) - } - - return v.duration() -} - -func (v Value) duration() time.Duration { - return time.Duration(int64(v.num)) -} - -// Float64 returns v's value as a float64. It panics -// if v is not a float64. -func (v Value) Float64() float64 { - if g, w := v.Kind(), KindFloat64; g != w { - panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) - } - - return v.float() -} - -func (v Value) float() float64 { - return math.Float64frombits(v.num) -} - -// Time returns v's value as a time.Time. It panics -// if v is not a time.Time. -func (v Value) Time() time.Time { - if g, w := v.Kind(), KindTime; g != w { - panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) - } - return v.time() -} - -func (v Value) time() time.Time { - loc := v.any.(timeLocation) - if loc == nil { - return time.Time{} - } - return time.Unix(0, int64(v.num)).In(loc) -} - -// LogValuer returns v's value as a LogValuer. It panics -// if v is not a LogValuer. -func (v Value) LogValuer() LogValuer { - return v.any.(LogValuer) -} - -// Group returns v's value as a []Attr. -// It panics if v's Kind is not KindGroup. -func (v Value) Group() []Attr { - if sp, ok := v.any.(groupptr); ok { - return unsafe.Slice((*Attr)(sp), v.num) - } - panic("Group: bad kind") -} - -func (v Value) group() []Attr { - return unsafe.Slice((*Attr)(v.any.(groupptr)), v.num) -} - -//////////////// Other - -// Equal reports whether v and w represent the same Go value. -func (v Value) Equal(w Value) bool { - k1 := v.Kind() - k2 := w.Kind() - if k1 != k2 { - return false - } - switch k1 { - case KindInt64, KindUint64, KindBool, KindDuration: - return v.num == w.num - case KindString: - return v.str() == w.str() - case KindFloat64: - return v.float() == w.float() - case KindTime: - return v.time().Equal(w.time()) - case KindAny, KindLogValuer: - return v.any == w.any // may panic if non-comparable - case KindGroup: - return slices.EqualFunc(v.group(), w.group(), Attr.Equal) - default: - panic(fmt.Sprintf("bad kind: %s", k1)) - } -} - -// append appends a text representation of v to dst. -// v is formatted as with fmt.Sprint. -func (v Value) append(dst []byte) []byte { - switch v.Kind() { - case KindString: - return append(dst, v.str()...) - case KindInt64: - return strconv.AppendInt(dst, int64(v.num), 10) - case KindUint64: - return strconv.AppendUint(dst, v.num, 10) - case KindFloat64: - return strconv.AppendFloat(dst, v.float(), 'g', -1, 64) - case KindBool: - return strconv.AppendBool(dst, v.bool()) - case KindDuration: - return append(dst, v.duration().String()...) - case KindTime: - return append(dst, v.time().String()...) - case KindGroup: - return fmt.Append(dst, v.group()) - case KindAny, KindLogValuer: - return fmt.Append(dst, v.any) - default: - panic(fmt.Sprintf("bad kind: %s", v.Kind())) - } -} - -// A LogValuer is any Go value that can convert itself into a Value for logging. -// -// This mechanism may be used to defer expensive operations until they are -// needed, or to expand a single value into a sequence of components. -type LogValuer interface { - LogValue() Value -} - -const maxLogValues = 100 - -// Resolve repeatedly calls LogValue on v while it implements LogValuer, -// and returns the result. -// If v resolves to a group, the group's attributes' values are not recursively -// resolved. -// If the number of LogValue calls exceeds a threshold, a Value containing an -// error is returned. -// Resolve's return value is guaranteed not to be of Kind KindLogValuer. -func (v Value) Resolve() (rv Value) { - orig := v - defer func() { - if r := recover(); r != nil { - rv = AnyValue(fmt.Errorf("LogValue panicked\n%s", stack(3, 5))) - } - }() - - for i := 0; i < maxLogValues; i++ { - if v.Kind() != KindLogValuer { - return v - } - v = v.LogValuer().LogValue() - } - err := fmt.Errorf("LogValue called too many times on Value of type %T", orig.Any()) - return AnyValue(err) -} - -func stack(skip, nFrames int) string { - pcs := make([]uintptr, nFrames+1) - n := runtime.Callers(skip+1, pcs) - if n == 0 { - return "(no stack)" - } - frames := runtime.CallersFrames(pcs[:n]) - var b strings.Builder - i := 0 - for { - frame, more := frames.Next() - fmt.Fprintf(&b, "called from %s (%s:%d)\n", frame.Function, frame.File, frame.Line) - if !more { - break - } - i++ - if i >= nFrames { - fmt.Fprintf(&b, "(rest of stack elided)\n") - break - } - } - return b.String() -} diff --git a/vendor/golang.org/x/exp/slog/value_119.go b/vendor/golang.org/x/exp/slog/value_119.go deleted file mode 100644 index 29b0d732..00000000 --- a/vendor/golang.org/x/exp/slog/value_119.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.19 && !go1.20 - -package slog - -import ( - "reflect" - "unsafe" -) - -type ( - stringptr unsafe.Pointer // used in Value.any when the Value is a string - groupptr unsafe.Pointer // used in Value.any when the Value is a []Attr -) - -// StringValue returns a new Value for a string. -func StringValue(value string) Value { - hdr := (*reflect.StringHeader)(unsafe.Pointer(&value)) - return Value{num: uint64(hdr.Len), any: stringptr(hdr.Data)} -} - -func (v Value) str() string { - var s string - hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) - hdr.Data = uintptr(v.any.(stringptr)) - hdr.Len = int(v.num) - return s -} - -// String returns Value's value as a string, formatted like fmt.Sprint. Unlike -// the methods Int64, Float64, and so on, which panic if v is of the -// wrong kind, String never panics. -func (v Value) String() string { - if sp, ok := v.any.(stringptr); ok { - // Inlining this code makes a huge difference. - var s string - hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) - hdr.Data = uintptr(sp) - hdr.Len = int(v.num) - return s - } - return string(v.append(nil)) -} - -// GroupValue returns a new Value for a list of Attrs. -// The caller must not subsequently mutate the argument slice. -func GroupValue(as ...Attr) Value { - hdr := (*reflect.SliceHeader)(unsafe.Pointer(&as)) - return Value{num: uint64(hdr.Len), any: groupptr(hdr.Data)} -} diff --git a/vendor/golang.org/x/exp/slog/value_120.go b/vendor/golang.org/x/exp/slog/value_120.go deleted file mode 100644 index f7d4c093..00000000 --- a/vendor/golang.org/x/exp/slog/value_120.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.20 - -package slog - -import "unsafe" - -type ( - stringptr *byte // used in Value.any when the Value is a string - groupptr *Attr // used in Value.any when the Value is a []Attr -) - -// StringValue returns a new Value for a string. -func StringValue(value string) Value { - return Value{num: uint64(len(value)), any: stringptr(unsafe.StringData(value))} -} - -// GroupValue returns a new Value for a list of Attrs. -// The caller must not subsequently mutate the argument slice. -func GroupValue(as ...Attr) Value { - return Value{num: uint64(len(as)), any: groupptr(unsafe.SliceData(as))} -} - -// String returns Value's value as a string, formatted like fmt.Sprint. Unlike -// the methods Int64, Float64, and so on, which panic if v is of the -// wrong kind, String never panics. -func (v Value) String() string { - if sp, ok := v.any.(stringptr); ok { - return unsafe.String(sp, v.num) - } - return string(v.append(nil)) -} - -func (v Value) str() string { - return unsafe.String(v.any.(stringptr), v.num) -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 9c3d5c7b..e2e7c15a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -108,7 +108,7 @@ github.com/hashicorp/hcl/json/token # github.com/inngest/expr v0.0.0-20241106234328-863dff7deec0 ## explicit; go 1.23.2 github.com/inngest/expr -# github.com/inngest/inngest v1.2.1-0.20241121162138-fa921e44acf1 +# github.com/inngest/inngest v1.3.1-0.20241122155305-f9a9e9ca9b15 ## explicit; go 1.23.2 github.com/inngest/inngest/pkg/connect/types github.com/inngest/inngest/pkg/connect/wsproto @@ -372,9 +372,6 @@ golang.org/x/crypto/salsa20/salsa ## explicit; go 1.22.0 golang.org/x/exp/constraints golang.org/x/exp/slices -golang.org/x/exp/slog -golang.org/x/exp/slog/internal -golang.org/x/exp/slog/internal/buffer # golang.org/x/mod v0.21.0 ## explicit; go 1.22.0 # golang.org/x/net v0.30.0 From 518197e459ae566329170264ef1e0a02fe6b0c31 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 17:00:41 +0100 Subject: [PATCH 12/16] only use outOfBandSync for ping deploy type --- handler.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/handler.go b/handler.go index f5c09cce..d5b38823 100644 --- a/handler.go +++ b/handler.go @@ -611,15 +611,10 @@ func (h *handler) outOfBandSync(w http.ResponseWriter, r *http.Request) error { pathAndParams := r.URL.String() - deployType := sdk.DeployTypePing - if h.useConnect { - deployType = sdk.DeployTypeConnect - } - config := sdk.RegisterRequest{ URL: fmt.Sprintf("%s://%s%s", scheme, host, pathAndParams), V: "1", - DeployType: deployType, + DeployType: sdk.DeployTypePing, SDK: HeaderValueSDK, AppName: h.appName, Headers: sdk.Headers{ From 0d95daad1512f6359fd7f91f760cd93292220184 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 17:28:24 +0100 Subject: [PATCH 13/16] start worker pool --- connect/handler.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/connect/handler.go b/connect/handler.go index 73eb15cb..cc8aee48 100644 --- a/connect/handler.go +++ b/connect/handler.go @@ -33,8 +33,10 @@ func Connect(ctx context.Context, opts Opts, invoker FunctionInvoker, logger *sl invoker: invoker, opts: opts, } + wp := NewWorkerPool(opts.WorkerConcurrency, ch.processExecutorRequest) ch.workerPool = wp + wp.Start(ctx) defer func() { // TODO Push remaining messages to another destination for processing? From 76bbde22ef73cff4eb2646d7e79f90e47ce23d2e Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Fri, 22 Nov 2024 17:29:12 +0100 Subject: [PATCH 14/16] always start worker pool immediately --- connect/handler.go | 3 +-- connect/worker_pool.go | 11 ++++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/connect/handler.go b/connect/handler.go index cc8aee48..d89a5610 100644 --- a/connect/handler.go +++ b/connect/handler.go @@ -34,9 +34,8 @@ func Connect(ctx context.Context, opts Opts, invoker FunctionInvoker, logger *sl opts: opts, } - wp := NewWorkerPool(opts.WorkerConcurrency, ch.processExecutorRequest) + wp := NewWorkerPool(ctx, opts.WorkerConcurrency, ch.processExecutorRequest) ch.workerPool = wp - wp.Start(ctx) defer func() { // TODO Push remaining messages to another destination for processing? diff --git a/connect/worker_pool.go b/connect/worker_pool.go index 0ac40514..4727f44c 100644 --- a/connect/worker_pool.go +++ b/connect/worker_pool.go @@ -19,7 +19,7 @@ type workerPool struct { workerPoolMsgs chan workerPoolMsg } -func NewWorkerPool(concurrency int, handler func(msg workerPoolMsg)) *workerPool { +func NewWorkerPool(ctx context.Context, concurrency int, handler func(msg workerPoolMsg)) *workerPool { wp := &workerPool{ // Should this use the same buffer size as the worker pool? workerPoolMsgs: make(chan workerPoolMsg, concurrency), @@ -27,16 +27,13 @@ func NewWorkerPool(concurrency int, handler func(msg workerPoolMsg)) *workerPool inProgress: sync.WaitGroup{}, handler: handler, } + for i := 0; i < wp.concurrency; i++ { + go wp.workerPool(ctx) + } return wp } -func (w *workerPool) Start(ctx context.Context) { - for i := 0; i < w.concurrency; i++ { - go w.workerPool(ctx) - } -} - func (w *workerPool) workerPool(ctx context.Context) { for { select { From c3096e17fd48e3a0e7bd2349e98b1e45c9306264 Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Mon, 25 Nov 2024 20:50:46 +0100 Subject: [PATCH 15/16] Move channels to connectHandler --- connect/connection.go | 18 +++++++++--------- connect/handler.go | 37 ++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/connect/connection.go b/connect/connection.go index 0c267eb6..f9767817 100644 --- a/connect/connection.go +++ b/connect/connection.go @@ -21,13 +21,13 @@ type connectReport struct { err error } -func (h *connectHandler) connect(ctx context.Context, data connectionEstablishData, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) { +func (h *connectHandler) connect(ctx context.Context, data connectionEstablishData) { // Set up connection (including connect handshake protocol) preparedConn, reconnect, err := h.prepareConnection(ctx, data) if err != nil { h.logger.Error("could not establish connection", "err", err) - notifyConnectDoneChan <- connectReport{ + h.notifyConnectDoneChan <- connectReport{ reconnect: reconnect, err: fmt.Errorf("could not establish connection: %w", err), } @@ -35,10 +35,10 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa } // Notify that the connection was established - notifyConnectedChan <- struct{}{} + h.notifyConnectedChan <- struct{}{} // Set up connection lifecycle logic (receiving messages, handling requests, etc.) - reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost, notifyConnectedChan, notifyConnectDoneChan) + reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost) if err != nil { h.logger.Error("could not handle connection", "err", err) @@ -47,14 +47,14 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa return } - notifyConnectDoneChan <- connectReport{ + h.notifyConnectDoneChan <- connectReport{ reconnect: reconnect, err: fmt.Errorf("could not handle connection: %w", err), } return } - notifyConnectDoneChan <- connectReport{ + h.notifyConnectDoneChan <- connectReport{ reconnect: reconnect, err: nil, } @@ -111,7 +111,7 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE return preparedConnection{ws, gatewayHost, connectionId.String()}, false, nil } -func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string, notifyConnectedChan chan struct{}, notifyConnectDoneChan chan connectReport) (reconnect bool, err error) { +func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string) (reconnect bool, err error) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -204,13 +204,13 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs // Intercept connected signal and pass it to the main goroutine notifyConnectedInterceptChan := make(chan struct{}) go func() { - <-notifyConnectedChan + <-h.notifyConnectedChan notifyConnectedInterceptChan <- struct{}{} doneWaiting() }() // Establish new connection and pass close reports back to the main goroutine - go h.connect(context.Background(), data, notifyConnectedInterceptChan, notifyConnectDoneChan) + go h.connect(context.Background(), data) cancel() diff --git a/connect/handler.go b/connect/handler.go index d89a5610..76146898 100644 --- a/connect/handler.go +++ b/connect/handler.go @@ -29,9 +29,12 @@ const ( func Connect(ctx context.Context, opts Opts, invoker FunctionInvoker, logger *slog.Logger) error { ch := &connectHandler{ - logger: logger, - invoker: invoker, - opts: opts, + logger: logger, + invoker: invoker, + opts: opts, + notifyConnectDoneChan: make(chan connectReport), + notifyConnectedChan: make(chan struct{}), + initiateConnectionChan: make(chan struct{}), } wp := NewWorkerPool(ctx, opts.WorkerConcurrency, ch.processExecutorRequest) @@ -91,6 +94,15 @@ type connectHandler struct { hostsManager *hostsManager workerPool *workerPool + + // Notify when connect finishes (either with an error or because the context got canceled) + notifyConnectDoneChan chan connectReport + + // Notify when connection is established + notifyConnectedChan chan struct{} + + // Channel to imperatively initiate a connection + initiateConnectionChan chan struct{} } // authContext is wrapper for information related to authentication @@ -127,15 +139,6 @@ func (h *connectHandler) Connect(ctx context.Context) error { h.hostsManager = newHostsManager(hosts) - // Notify when connect finishes (either with an error or because the context got canceled) - notifyConnectDoneChan := make(chan connectReport) - - // Notify when connection is established - notifyConnectedChan := make(chan struct{}) - - // Channel to imperatively initiate a connection - initiateConnectionChan := make(chan struct{}) - var attempts int // We construct a connection loop, which will attempt to reconnect on failure @@ -151,12 +154,12 @@ func (h *connectHandler) Connect(ctx context.Context) error { return nil // Reset attempts when connection succeeded - case <-notifyConnectedChan: + case <-h.notifyConnectedChan: attempts = 0 continue // Handle connection done events - case msg := <-notifyConnectDoneChan: + case msg := <-h.notifyConnectDoneChan: h.logger.Error("connect failed", "err", err, "reconnect", msg.reconnect) if !msg.reconnect { @@ -196,7 +199,7 @@ func (h *connectHandler) Connect(ctx context.Context) error { // continue to reconnect logic h.logger.Debug("reconnecting", "attempts", attempts) - case <-initiateConnectionChan: + case <-h.initiateConnectionChan: } if attempts == 5 { @@ -211,12 +214,12 @@ func (h *connectHandler) Connect(ctx context.Context) error { totalMem: int64(totalMem), marshaledFns: marshaledFns, marshaledCapabilities: marshaledCapabilities, - }, notifyConnectedChan, notifyConnectDoneChan) + }) } }) // Initiate the first connection - initiateConnectionChan <- struct{}{} + h.initiateConnectionChan <- struct{}{} // Wait until connection loop finishes if err := eg.Wait(); err != nil { From 1abd9001507e534585496bcd630411f2d29618ea Mon Sep 17 00:00:00 2001 From: Bruno Scheufler Date: Mon, 25 Nov 2024 21:48:07 +0100 Subject: [PATCH 16/16] clean up return types --- connect/connection.go | 51 ++++++++++++++++++++++--------------------- connect/handshake.go | 33 +++++++++++++++++++++------- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/connect/connection.go b/connect/connection.go index f9767817..695965bf 100644 --- a/connect/connection.go +++ b/connect/connection.go @@ -23,12 +23,12 @@ type connectReport struct { func (h *connectHandler) connect(ctx context.Context, data connectionEstablishData) { // Set up connection (including connect handshake protocol) - preparedConn, reconnect, err := h.prepareConnection(ctx, data) + preparedConn, err := h.prepareConnection(ctx, data) if err != nil { h.logger.Error("could not establish connection", "err", err) h.notifyConnectDoneChan <- connectReport{ - reconnect: reconnect, + reconnect: shouldReconnect(err), err: fmt.Errorf("could not establish connection: %w", err), } return @@ -38,7 +38,7 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa h.notifyConnectedChan <- struct{}{} // Set up connection lifecycle logic (receiving messages, handling requests, etc.) - reconnect, err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost) + err = h.handleConnection(ctx, data, preparedConn.ws, preparedConn.gatewayHost) if err != nil { h.logger.Error("could not handle connection", "err", err) @@ -48,16 +48,13 @@ func (h *connectHandler) connect(ctx context.Context, data connectionEstablishDa } h.notifyConnectDoneChan <- connectReport{ - reconnect: reconnect, + reconnect: shouldReconnect(err), err: fmt.Errorf("could not handle connection: %w", err), } return } - h.notifyConnectDoneChan <- connectReport{ - reconnect: reconnect, - err: nil, - } + h.notifyConnectDoneChan <- connectReport{} } type connectionEstablishData struct { @@ -69,13 +66,13 @@ type connectionEstablishData struct { manualReadinessAck bool } -type preparedConnection struct { +type connection struct { ws *websocket.Conn gatewayHost string connectionId string } -func (h *connectHandler) prepareConnection(ctx context.Context, data connectionEstablishData) (preparedConnection, bool, error) { +func (h *connectHandler) prepareConnection(ctx context.Context, data connectionEstablishData) (connection, error) { connectTimeout, cancelConnectTimeout := context.WithTimeout(ctx, 10*time.Second) defer cancelConnectTimeout() @@ -84,7 +81,7 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE // All gateways have been tried, reset the internal state to retry h.hostsManager.resetGateways() - return preparedConnection{}, true, fmt.Errorf("no available gateway hosts") + return connection{}, reconnectError{fmt.Errorf("no available gateway hosts")} } // Establish WebSocket connection to one of the gateways @@ -95,7 +92,7 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE }) if err != nil { h.hostsManager.markUnreachableGateway(gatewayHost) - return preparedConnection{}, true, fmt.Errorf("could not connect to gateway: %w", err) + return connection{}, reconnectError{fmt.Errorf("could not connect to gateway: %w", err)} } // Connection ID is unique per connection, reconnections should get a new ID @@ -103,15 +100,19 @@ func (h *connectHandler) prepareConnection(ctx context.Context, data connectionE h.logger.Debug("websocket connection established", "gateway_host", gatewayHost) - reconnect, err := h.performConnectHandshake(ctx, connectionId.String(), ws, gatewayHost, data) + err = h.performConnectHandshake(ctx, connectionId.String(), ws, gatewayHost, data) if err != nil { - return preparedConnection{}, reconnect, fmt.Errorf("could not perform connect handshake: %w", err) + return connection{}, reconnectError{fmt.Errorf("could not perform connect handshake: %w", err)} } - return preparedConnection{ws, gatewayHost, connectionId.String()}, false, nil + return connection{ + ws: ws, + gatewayHost: gatewayHost, + connectionId: connectionId.String(), + }, nil } -func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string) (reconnect bool, err error) { +func (h *connectHandler) handleConnection(ctx context.Context, data connectionEstablishData, ws *websocket.Conn, gatewayHost string) error { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -130,9 +131,9 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs // Send buffered but unsent messages if connection was re-established if len(h.messageBuffer) > 0 { h.logger.Debug("sending buffered messages", "count", len(h.messageBuffer)) - err = h.sendBufferedMessages(ws) + err := h.sendBufferedMessages(ws) if err != nil { - return true, fmt.Errorf("could not send buffered messages: %w", err) + return reconnectError{fmt.Errorf("could not send buffered messages: %w", err)} } } @@ -159,7 +160,7 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs eg.Go(func() error { for { var msg connectproto.ConnectMessage - err = wsproto.Read(context.Background(), ws, &msg) + err := wsproto.Read(context.Background(), ws, &msg) if err != nil { h.logger.Error("failed to read message", "err", err) @@ -222,7 +223,7 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs } // By returning, we will close the old connection - return false, errGatewayDraining + return errGatewayDraining } h.logger.Debug("read loop ended with error", "err", err) @@ -233,17 +234,17 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs h.logger.Error("connection closed with reason", "reason", cerr.Reason) // Reconnect! - return true, fmt.Errorf("connection closed with reason %q: %w", cerr.Reason, cerr) + return reconnectError{fmt.Errorf("connection closed with reason %q: %w", cerr.Reason, cerr)} } // connection closed without reason if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { h.logger.Error("failed to read message from gateway, lost connection unexpectedly", "err", err) - return true, fmt.Errorf("connection closed unexpectedly: %w", cerr) + return reconnectError{fmt.Errorf("connection closed unexpectedly: %w", cerr)} } // If this is not a worker shutdown, we should reconnect - return true, fmt.Errorf("connection closed unexpectedly: %w", ctx.Err()) + return reconnectError{fmt.Errorf("connection closed unexpectedly: %w", ctx.Err())} } // Perform graceful shutdown routine (context was cancelled) @@ -268,7 +269,7 @@ func (h *connectHandler) handleConnection(ctx context.Context, data connectionEs // Attempt to shut down connection if not already done _ = ws.Close(websocket.StatusNormalClosure, connectproto.WorkerDisconnectReason_WORKER_SHUTDOWN.String()) - return false, nil + return nil } func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, handler func(ws *websocket.Conn) error) error { @@ -284,7 +285,7 @@ func (h *connectHandler) withTemporaryConnection(data connectionEstablishData, h return fmt.Errorf("could not establish connection after %d attempts", maxAttempts) } - ws, _, err := h.prepareConnection(context.Background(), data) + ws, err := h.prepareConnection(context.Background(), data) if err != nil { attempts++ continue diff --git a/connect/handshake.go b/connect/handshake.go index 1135d1eb..48082326 100644 --- a/connect/handshake.go +++ b/connect/handshake.go @@ -2,6 +2,7 @@ package connect import ( "context" + "errors" "fmt" "github.com/coder/websocket" "github.com/inngest/inngest/pkg/connect/wsproto" @@ -11,7 +12,23 @@ import ( "time" ) -func (h *connectHandler) performConnectHandshake(ctx context.Context, connectionId string, ws *websocket.Conn, gatewayHost string, data connectionEstablishData) (bool, error) { +type reconnectError struct { + err error +} + +func (e reconnectError) Unwrap() error { + return e.err +} + +func (e reconnectError) Error() string { + return fmt.Sprintf("reconnect error: %v", e.err) +} + +func shouldReconnect(err error) bool { + return errors.Is(err, reconnectError{}) +} + +func (h *connectHandler) performConnectHandshake(ctx context.Context, connectionId string, ws *websocket.Conn, gatewayHost string, data connectionEstablishData) error { // Wait for gateway hello message { initialMessageTimeout, cancelInitialTimeout := context.WithTimeout(ctx, 5*time.Second) @@ -20,12 +37,12 @@ func (h *connectHandler) performConnectHandshake(ctx context.Context, connection err := wsproto.Read(initialMessageTimeout, ws, &helloMessage) if err != nil { h.hostsManager.markUnreachableGateway(gatewayHost) - return true, fmt.Errorf("did not receive gateway hello message: %w", err) + return reconnectError{fmt.Errorf("did not receive gateway hello message: %w", err)} } if helloMessage.Kind != connectproto.GatewayMessageType_GATEWAY_HELLO { h.hostsManager.markUnreachableGateway(gatewayHost) - return true, fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind) + return reconnectError{fmt.Errorf("expected gateway hello message, got %s", helloMessage.Kind)} } h.logger.Debug("received gateway hello message") @@ -66,7 +83,7 @@ func (h *connectHandler) performConnectHandshake(ctx context.Context, connection WorkerManualReadinessAck: data.manualReadinessAck, }) if err != nil { - return false, fmt.Errorf("could not serialize sdk connect message: %w", err) + return fmt.Errorf("could not serialize sdk connect message: %w", err) } err = wsproto.Write(ctx, ws, &connectproto.ConnectMessage{ @@ -74,7 +91,7 @@ func (h *connectHandler) performConnectHandshake(ctx context.Context, connection Payload: data, }) if err != nil { - return true, fmt.Errorf("could not send initial message") + return reconnectError{fmt.Errorf("could not send initial message")} } } @@ -85,15 +102,15 @@ func (h *connectHandler) performConnectHandshake(ctx context.Context, connection var connectionReadyMsg connectproto.ConnectMessage err := wsproto.Read(connectionReadyTimeout, ws, &connectionReadyMsg) if err != nil { - return true, fmt.Errorf("did not receive gateway connection ready message: %w", err) + return reconnectError{fmt.Errorf("did not receive gateway connection ready message: %w", err)} } if connectionReadyMsg.Kind != connectproto.GatewayMessageType_GATEWAY_CONNECTION_READY { - return true, fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind) + return reconnectError{fmt.Errorf("expected gateway connection ready message, got %s", connectionReadyMsg.Kind)} } h.logger.Debug("received gateway connection ready message") } - return false, nil + return nil }