From 0589ddc44a8b37bee308c414d804cb45d063fea2 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 3 Jun 2025 14:31:34 +0200 Subject: [PATCH 01/61] WIP Signed-off-by: Danny Kopping --- Makefile | 9 + aibridged/aibridged.go | 291 ++++++++++++++++++++ aibridged/http_proxy.go | 118 ++++++++ aibridged/interface.go | 39 +++ aibridged/proto/aibridged.pb.go | 293 ++++++++++++++++++++ aibridged/proto/aibridged.proto | 35 +++ aibridged/proto/aibridged_drpc.pb.go | 317 ++++++++++++++++++++++ cli/server.go | 69 +++++ coderd/aibridge.go | 57 ++++ coderd/aibridgedserver/aibridgedserver.go | 24 ++ coderd/coderd.go | 143 +++++++++- coderd/util/slice/slice.go | 22 ++ codersdk/deployment.go | 21 ++ 13 files changed, 1427 insertions(+), 11 deletions(-) create mode 100644 aibridged/aibridged.go create mode 100644 aibridged/http_proxy.go create mode 100644 aibridged/interface.go create mode 100644 aibridged/proto/aibridged.pb.go create mode 100644 aibridged/proto/aibridged.proto create mode 100644 aibridged/proto/aibridged_drpc.pb.go create mode 100644 coderd/aibridge.go create mode 100644 coderd/aibridgedserver/aibridgedserver.go diff --git a/Makefile b/Makefile index 0b8cefbab0663..dec29e80d702d 100644 --- a/Makefile +++ b/Makefile @@ -566,6 +566,7 @@ GEN_FILES := \ provisionersdk/proto/provisioner.pb.go \ provisionerd/proto/provisionerd.pb.go \ vpn/vpn.pb.go \ + aibridged/proto/aibridged.pb.go \ $(DB_GEN_FILES) \ $(SITE_GEN_FILES) \ coderd/rbac/object_gen.go \ @@ -725,6 +726,14 @@ vpn/vpn.pb.go: vpn/vpn.proto --go_opt=paths=source_relative \ ./vpn/vpn.proto +aibridged/proto/aibridged.pb.go: aibridged/proto/aibridged.proto + protoc \ + --go_out=. \ + --go_opt=paths=source_relative \ + --go-drpc_out=. \ + --go-drpc_opt=paths=source_relative \ + ./aibridged/proto/aibridged.proto + site/src/api/typesGenerated.ts: site/node_modules/.installed $(wildcard scripts/apitypings/*) $(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go') # -C sets the directory for the go run command go run -C ./scripts/apitypings main.go > $@ diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go new file mode 100644 index 0000000000000..bbf0ef640a149 --- /dev/null +++ b/aibridged/aibridged.go @@ -0,0 +1,291 @@ +package aibridged + +import ( + "context" + "errors" + "io" + "net/http" + "sync" + "time" + + "cdr.dev/slog" + "github.com/coder/retry" + "github.com/hashicorp/yamux" + "github.com/valyala/fasthttp/fasthttputil" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/aibridged/proto" + "github.com/coder/coder/v2/codersdk" +) + +type Dialer func(ctx context.Context) (proto.DRPCAIBridgeDaemonClient, error) + +type Server struct { + clientDialer Dialer + clientCh chan proto.DRPCAIBridgeDaemonClient + + logger slog.Logger + wg sync.WaitGroup + + // initConnectionCh will receive when the daemon connects to coderd for the + // first time. + initConnectionCh chan struct{} + initConnectionOnce sync.Once + + // mutex protects all subsequent fields + mutex sync.Mutex + // closeContext is canceled when we start closing. + closeContext context.Context + closeCancel context.CancelFunc + // closeError stores the error when closing to return to subsequent callers + closeError error + // closingB is set to true when we start closing + closingB bool + // closedCh will receive when we complete closing + closedCh chan struct{} + // shuttingDownB is set to true when we start graceful shutdown + shuttingDownB bool + // shuttingDownCh will receive when we start graceful shutdown + shuttingDownCh chan struct{} +} + +func New(clientDialer Dialer, logger slog.Logger) (*Server, error) { + ctx, ctxCancel := context.WithCancel(context.Background()) + daemon := &Server{ + logger: logger, + clientDialer: clientDialer, + clientCh: make(chan proto.DRPCAIBridgeDaemonClient), + closeContext: ctx, + closeCancel: ctxCancel, + closedCh: make(chan struct{}), + shuttingDownCh: make(chan struct{}), + initConnectionCh: make(chan struct{}), + } + go daemon.connect() + + return daemon, nil +} // Connect establishes a connection to coderd. +func (s *Server) connect() { + defer s.logger.Debug(s.closeContext, "connect loop exited") + defer s.wg.Done() + logConnect := s.logger.Debug + // An exponential back-off occurs when the connection is failing to dial. + // This is to prevent server spam in case of a coderd outage. +connectLoop: + for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(s.closeContext); { + // TODO(dannyk): handle premature close. + //// It's possible for the provisioner daemon to be shut down + //// before the wait is complete! + //if s.isClosed() { + // return + //} + + s.logger.Debug(s.closeContext, "dialing coderd") + client, err := s.clientDialer(s.closeContext) + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + var sdkErr *codersdk.Error + // If something is wrong with our auth, stop trying to connect. + if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusForbidden { + s.logger.Error(s.closeContext, "not authorized to dial coderd", slog.Error(err)) + return + } + if s.isClosed() { + return + } + s.logger.Warn(s.closeContext, "coderd client failed to dial", slog.Error(err)) + continue + } + + // This log is useful to verify that an external provisioner daemon is + // successfully connecting to coderd. It doesn't add much value if the + // daemon is built-in, so we only log it on the info level if p.externalProvisioner + // is true. This log message is mentioned in the docs: + // https://github.com/coder/coder/blob/5bd86cb1c06561d1d3e90ce689da220467e525c0/docs/admin/provisioners.md#L346 + logConnect(s.closeContext, "successfully connected to coderd") + retrier.Reset() + s.initConnectionOnce.Do(func() { + close(s.initConnectionCh) + }) + + // serve the client until we are closed or it disconnects + for { + select { + case <-s.closeContext.Done(): + client.DRPCConn().Close() + return + case <-client.DRPCConn().Closed(): + logConnect(s.closeContext, "connection to coderd closed") + continue connectLoop + case s.clientCh <- client: + continue + } + } + } +} + +func (s *Server) client() (proto.DRPCAIBridgeDaemonClient, bool) { + select { + case <-s.closeContext.Done(): + return nil, false + case <-s.shuttingDownCh: + // Shutting down should return a nil client and unblock + return nil, false + case client := <-s.clientCh: + return client, true + } +} + +func (s *Server) AuditPrompt(ctx context.Context, in *proto.AuditPromptRequest) (*proto.AuditPromptResponse, error) { + out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.AuditPromptResponse, error) { + return client.AuditPrompt(ctx, in) + }) + if err != nil { + return nil, err + } + return out, nil +} + +func (s *Server) ChatCompletions(payload *proto.JSONPayload, stream proto.DRPCOpenAIService_ChatCompletionsStream) error { + // TODO: call OpenAI API. + + select { + case <-stream.Context().Done(): + return nil + default: + } + + err := stream.Send(&proto.JSONPayload{ + Content: ` +{ + "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT", + "object": "chat.completion", + "created": 1741569952, + "model": "gpt-4.1-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 19, + "completion_tokens": 10, + "total_tokens": 29, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default" +} +`}) + if err != nil { + return xerrors.Errorf("stream chat completion response: %w", err) + } + return nil +} + +// TODO: direct copy/paste from provisionerd, abstract into common util. +func retryable(err error) bool { + return xerrors.Is(err, yamux.ErrSessionShutdown) || xerrors.Is(err, io.EOF) || xerrors.Is(err, fasthttputil.ErrInmemoryListenerClosed) || + // annoyingly, dRPC sometimes returns context.Canceled if the transport was closed, even if the context for + // the RPC *is not canceled*. Retrying is fine if the RPC context is not canceled. + xerrors.Is(err, context.Canceled) +} + +// clientDoWithRetries runs the function f with a client, and retries with +// backoff until either the error returned is not retryable() or the context +// expires. +// TODO: direct copy/paste from provisionerd, abstract into common util. +func clientDoWithRetries[T any](ctx context.Context, + getClient func() (proto.DRPCAIBridgeDaemonClient, bool), + f func(context.Context, proto.DRPCAIBridgeDaemonClient) (T, error), +) (ret T, _ error) { + for retrier := retry.New(25*time.Millisecond, 5*time.Second); retrier.Wait(ctx); { + client, ok := getClient() + if !ok { + continue + } + resp, err := f(ctx, client) + if retryable(err) { + continue + } + return resp, err + } + return ret, ctx.Err() +} + +// isClosed returns whether the API is closed or not. +func (s *Server) isClosed() bool { + select { + case <-s.closeContext.Done(): + return true + default: + return false + } +} + +// closeWithError closes the provisioner; subsequent reads/writes will return the error err. +func (s *Server) closeWithError(err error) error { + s.mutex.Lock() + first := false + if !s.closingB { + first = true + s.closingB = true + } + // don't hold the mutex while doing I/O. + s.mutex.Unlock() + + if first { + s.closeCancel() + s.logger.Debug(context.Background(), "waiting for goroutines to exit") + s.wg.Wait() + s.logger.Debug(context.Background(), "closing server with error", slog.Error(err)) + s.closeError = err + close(s.closedCh) + return err + } + s.logger.Debug(s.closeContext, "waiting for first closer to complete") + <-s.closedCh + s.logger.Debug(s.closeContext, "first closer completed") + return s.closeError +} + +// Close ends the aibridge daemon. +func (s *Server) Close() error { + if s == nil { + return nil + } + + s.logger.Info(s.closeContext, "closing aibridged") + // TODO: invalidate all running requests (cancelling context should be enough?). + errMsg := "aibridged closed gracefully" + err := s.closeWithError(nil) + if err != nil { + errMsg = err.Error() + } + s.logger.Warn(s.closeContext, errMsg) + + return err +} + +func (s *Server) Shutdown(ctx context.Context) error { + // TODO: implement or remove. + return nil +} diff --git a/aibridged/http_proxy.go b/aibridged/http_proxy.go new file mode 100644 index 0000000000000..c0b3e8f0dfcb2 --- /dev/null +++ b/aibridged/http_proxy.go @@ -0,0 +1,118 @@ +package aibridged + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "cdr.dev/slog" + "storj.io/drpc/drpcmux" + + "github.com/coder/coder/v2/aibridged/proto" +) + +type AIProvider string + +const ( + AIProviderOpenAI AIProvider = "openai" + AIProviderAnthropic AIProvider = "anthropic" +) + +type ProxyConfig struct { + ReadTimeout time.Duration +} + +type HTTPProxy struct { + logger slog.Logger + provider AIProvider + config ProxyConfig + mux *drpcmux.Mux + drpcClient AIServiceClient +} + +// NewDRPCProxy creates a new reverse proxy instance. +func NewDRPCProxy(client AIServiceClient, config ProxyConfig) (*HTTPProxy, error) { + return &HTTPProxy{ + config: config, + mux: drpcmux.New(), + drpcClient: client, + }, nil +} // ServeHTTP handles incoming HTTP requests and proxies them to dRPC + +func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Read request body + body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MiB // TODO: make configurable. + if err != nil { + http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest) + return + } + defer r.Body.Close() + + // TODO: use body + _ = body + + // Create context with timeout + ctx, cancel := context.WithTimeout(r.Context(), p.config.ReadTimeout) + defer cancel() + + switch p.drpcClient.(type) { + case *OpenAIAdapter: + payload := struct { + messages []struct { + Role string `json:"role,omitempty"` + Content string `json:"content,omitempty"` + } + model string + }{ + messages: []struct { + Role string `json:"role,omitempty"` + Content string `json:"content,omitempty"` + }{ + { + Role: "developer", + Content: "what is 9/0?", + }, + }, + model: "gpt-4o", + } + payloadJSON, err := json.Marshal(payload) + if err != nil { + // TODO: better error. + http.Error(w, "failed to marshal payload", http.StatusInternalServerError) + return + } + + resp, err := p.drpcClient.SendRequest(ctx, &proto.JSONPayload{ + Content: string(payloadJSON), + }) + if err != nil { + return + } + + // TODO: start SSE + for { + respPayload, err := resp.Recv() + if err != nil { + if err == io.EOF { + return + } + http.Error(w, "failed to receive payload", http.StatusInternalServerError) + return + } + + _, _ = w.Write([]byte(respPayload.Content)) + } + + // Set appropriate headers + //w.Header().Set("Content-Type", "application/json") + + // Write response + //if _, err := w.Write([]byte(resp.)); err != nil { + // p.logger.Warn(ctx, "failed to write response: %w", err) + //} + case *AnthropicAdapter: + } +} diff --git a/aibridged/interface.go b/aibridged/interface.go new file mode 100644 index 0000000000000..665952d743f37 --- /dev/null +++ b/aibridged/interface.go @@ -0,0 +1,39 @@ +package aibridged + +import ( + "context" + + "storj.io/drpc" + + "github.com/coder/coder/v2/aibridged/proto" +) + +// Define a common interface for AI services +type AIServiceClient interface { + SendRequest(ctx context.Context, payload *proto.JSONPayload) (StreamingResponder[*proto.JSONPayload], error) +} + +type StreamingResponder[T any] interface { + drpc.Stream + Recv() (T, error) +} + +type OpenAIAdapter struct { + client proto.DRPCOpenAIServiceClient +} + +func NewOpenAIAdapter(client proto.DRPCOpenAIServiceClient) *OpenAIAdapter { + return &OpenAIAdapter{client: client} +} + +func (a *OpenAIAdapter) SendRequest(ctx context.Context, payload *proto.JSONPayload) (StreamingResponder[*proto.JSONPayload], error) { + return a.client.ChatCompletions(ctx, payload) +} + +type AnthropicAdapter struct { + client proto.DRPCAnthropicServiceClient +} + +func (a *AnthropicAdapter) SendRequest(ctx context.Context, payload *proto.JSONPayload) (StreamingResponder[*proto.JSONPayload], error) { + return a.client.Messages(ctx, payload) +} diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go new file mode 100644 index 0000000000000..0bc64f0bf7b86 --- /dev/null +++ b/aibridged/proto/aibridged.pb.go @@ -0,0 +1,293 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v4.23.4 +// source: aibridged/proto/aibridged.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AuditPromptRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Prompt string `protobuf:"bytes,1,opt,name=prompt,proto3" json:"prompt,omitempty"` + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` +} + +func (x *AuditPromptRequest) Reset() { + *x = AuditPromptRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuditPromptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuditPromptRequest) ProtoMessage() {} + +func (x *AuditPromptRequest) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuditPromptRequest.ProtoReflect.Descriptor instead. +func (*AuditPromptRequest) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} +} + +func (x *AuditPromptRequest) GetPrompt() string { + if x != nil { + return x.Prompt + } + return "" +} + +func (x *AuditPromptRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +type AuditPromptResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *AuditPromptResponse) Reset() { + *x = AuditPromptResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuditPromptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuditPromptResponse) ProtoMessage() {} + +func (x *AuditPromptResponse) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuditPromptResponse.ProtoReflect.Descriptor instead. +func (*AuditPromptResponse) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} +} + +type JSONPayload struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` // Rather than modelling the full API in proto, let's just accept the JSON payload and naïvely pass it along. +} + +func (x *JSONPayload) Reset() { + *x = JSONPayload{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *JSONPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JSONPayload) ProtoMessage() {} + +func (x *JSONPayload) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JSONPayload.ProtoReflect.Descriptor instead. +func (*JSONPayload) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} +} + +func (x *JSONPayload) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor + +var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ + 0x0a, 0x1f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x09, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x12, + 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x13, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, + 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x0a, + 0x0b, 0x4a, 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x32, 0x5e, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, + 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, + 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1d, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, + 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, + 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x54, 0x0a, 0x0d, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x0f, 0x43, 0x68, 0x61, 0x74, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x69, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, 0x6c, 0x6f, + 0x61, 0x64, 0x1a, 0x16, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x4a, + 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x30, 0x01, 0x32, 0x50, 0x0a, 0x10, + 0x41, 0x6e, 0x74, 0x68, 0x72, 0x6f, 0x70, 0x69, 0x63, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x3c, 0x0a, 0x08, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x1a, 0x16, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, + 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x30, 0x01, 0x42, 0x2b, + 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, + 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_aibridged_proto_aibridged_proto_rawDescOnce sync.Once + file_aibridged_proto_aibridged_proto_rawDescData = file_aibridged_proto_aibridged_proto_rawDesc +) + +func file_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { + file_aibridged_proto_aibridged_proto_rawDescOnce.Do(func() { + file_aibridged_proto_aibridged_proto_rawDescData = protoimpl.X.CompressGZIP(file_aibridged_proto_aibridged_proto_rawDescData) + }) + return file_aibridged_proto_aibridged_proto_rawDescData +} + +var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ + (*AuditPromptRequest)(nil), // 0: aibridged.AuditPromptRequest + (*AuditPromptResponse)(nil), // 1: aibridged.AuditPromptResponse + (*JSONPayload)(nil), // 2: aibridged.JSONPayload +} +var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ + 0, // 0: aibridged.AIBridgeDaemon.AuditPrompt:input_type -> aibridged.AuditPromptRequest + 2, // 1: aibridged.OpenAIService.ChatCompletions:input_type -> aibridged.JSONPayload + 2, // 2: aibridged.AnthropicService.Messages:input_type -> aibridged.JSONPayload + 1, // 3: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse + 2, // 4: aibridged.OpenAIService.ChatCompletions:output_type -> aibridged.JSONPayload + 2, // 5: aibridged.AnthropicService.Messages:output_type -> aibridged.JSONPayload + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_aibridged_proto_aibridged_proto_init() } +func file_aibridged_proto_aibridged_proto_init() { + if File_aibridged_proto_aibridged_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_aibridged_proto_aibridged_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuditPromptRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_aibridged_proto_aibridged_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuditPromptResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_aibridged_proto_aibridged_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*JSONPayload); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_aibridged_proto_aibridged_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 3, + }, + GoTypes: file_aibridged_proto_aibridged_proto_goTypes, + DependencyIndexes: file_aibridged_proto_aibridged_proto_depIdxs, + MessageInfos: file_aibridged_proto_aibridged_proto_msgTypes, + }.Build() + File_aibridged_proto_aibridged_proto = out.File + file_aibridged_proto_aibridged_proto_rawDesc = nil + file_aibridged_proto_aibridged_proto_goTypes = nil + file_aibridged_proto_aibridged_proto_depIdxs = nil +} diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto new file mode 100644 index 0000000000000..23e1540af1c34 --- /dev/null +++ b/aibridged/proto/aibridged.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; +option go_package = "github.com/coder/coder/v2/aibridged/proto"; + +package aibridged; + +message AuditPromptRequest { + string prompt = 1; + string provider = 2; +} + +message AuditPromptResponse {} + +// AIBridgeDaemon describes the service exposed by coderd, which allows the AI bridge to send data to coderd. +service AIBridgeDaemon { + + // TODO: expand to full auditing; this is just a toy implementation for the moment. + // AuditPrompt allows aibridged to report back to coderd that a prompt was issued by a user to an AI provider. + rpc AuditPrompt(AuditPromptRequest) returns (AuditPromptResponse); +} + +message JSONPayload { + string content = 1; // Rather than modelling the full API in proto, let's just accept the JSON payload and naïvely pass it along. +} + +// OpenAIService describes the service exposed by aibridged, which allows coderd to proxy AI requests to it. +// TODO(dannyk): versioning of both upstream service and this service. +service OpenAIService { + rpc ChatCompletions(JSONPayload) returns (stream JSONPayload); +} + +// AnthropicService describes the service exposed by aibridged, which allows coderd to proxy AI requests to it. +// TODO(dannyk): versioning of both upstream service and this service. +service AnthropicService { + rpc Messages(JSONPayload) returns (stream JSONPayload); +} diff --git a/aibridged/proto/aibridged_drpc.pb.go b/aibridged/proto/aibridged_drpc.pb.go new file mode 100644 index 0000000000000..7aa99e182285b --- /dev/null +++ b/aibridged/proto/aibridged_drpc.pb.go @@ -0,0 +1,317 @@ +// Code generated by protoc-gen-go-drpc. DO NOT EDIT. +// protoc-gen-go-drpc version: v0.0.34 +// source: aibridged/proto/aibridged.proto + +package proto + +import ( + context "context" + errors "errors" + protojson "google.golang.org/protobuf/encoding/protojson" + proto "google.golang.org/protobuf/proto" + drpc "storj.io/drpc" + drpcerr "storj.io/drpc/drpcerr" +) + +type drpcEncoding_File_aibridged_proto_aibridged_proto struct{} + +func (drpcEncoding_File_aibridged_proto_aibridged_proto) Marshal(msg drpc.Message) ([]byte, error) { + return proto.Marshal(msg.(proto.Message)) +} + +func (drpcEncoding_File_aibridged_proto_aibridged_proto) MarshalAppend(buf []byte, msg drpc.Message) ([]byte, error) { + return proto.MarshalOptions{}.MarshalAppend(buf, msg.(proto.Message)) +} + +func (drpcEncoding_File_aibridged_proto_aibridged_proto) Unmarshal(buf []byte, msg drpc.Message) error { + return proto.Unmarshal(buf, msg.(proto.Message)) +} + +func (drpcEncoding_File_aibridged_proto_aibridged_proto) JSONMarshal(msg drpc.Message) ([]byte, error) { + return protojson.Marshal(msg.(proto.Message)) +} + +func (drpcEncoding_File_aibridged_proto_aibridged_proto) JSONUnmarshal(buf []byte, msg drpc.Message) error { + return protojson.Unmarshal(buf, msg.(proto.Message)) +} + +type DRPCAIBridgeDaemonClient interface { + DRPCConn() drpc.Conn + + AuditPrompt(ctx context.Context, in *AuditPromptRequest) (*AuditPromptResponse, error) +} + +type drpcAIBridgeDaemonClient struct { + cc drpc.Conn +} + +func NewDRPCAIBridgeDaemonClient(cc drpc.Conn) DRPCAIBridgeDaemonClient { + return &drpcAIBridgeDaemonClient{cc} +} + +func (c *drpcAIBridgeDaemonClient) DRPCConn() drpc.Conn { return c.cc } + +func (c *drpcAIBridgeDaemonClient) AuditPrompt(ctx context.Context, in *AuditPromptRequest) (*AuditPromptResponse, error) { + out := new(AuditPromptResponse) + err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/AuditPrompt", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + +type DRPCAIBridgeDaemonServer interface { + AuditPrompt(context.Context, *AuditPromptRequest) (*AuditPromptResponse, error) +} + +type DRPCAIBridgeDaemonUnimplementedServer struct{} + +func (s *DRPCAIBridgeDaemonUnimplementedServer) AuditPrompt(context.Context, *AuditPromptRequest) (*AuditPromptResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +type DRPCAIBridgeDaemonDescription struct{} + +func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 1 } + +func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { + switch n { + case 0: + return "/aibridged.AIBridgeDaemon/AuditPrompt", drpcEncoding_File_aibridged_proto_aibridged_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAIBridgeDaemonServer). + AuditPrompt( + ctx, + in1.(*AuditPromptRequest), + ) + }, DRPCAIBridgeDaemonServer.AuditPrompt, true + default: + return "", nil, nil, nil, false + } +} + +func DRPCRegisterAIBridgeDaemon(mux drpc.Mux, impl DRPCAIBridgeDaemonServer) error { + return mux.Register(impl, DRPCAIBridgeDaemonDescription{}) +} + +type DRPCAIBridgeDaemon_AuditPromptStream interface { + drpc.Stream + SendAndClose(*AuditPromptResponse) error +} + +type drpcAIBridgeDaemon_AuditPromptStream struct { + drpc.Stream +} + +func (x *drpcAIBridgeDaemon_AuditPromptStream) SendAndClose(m *AuditPromptResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { + return err + } + return x.CloseSend() +} + +type DRPCOpenAIServiceClient interface { + DRPCConn() drpc.Conn + + ChatCompletions(ctx context.Context, in *JSONPayload) (DRPCOpenAIService_ChatCompletionsClient, error) +} + +type drpcOpenAIServiceClient struct { + cc drpc.Conn +} + +func NewDRPCOpenAIServiceClient(cc drpc.Conn) DRPCOpenAIServiceClient { + return &drpcOpenAIServiceClient{cc} +} + +func (c *drpcOpenAIServiceClient) DRPCConn() drpc.Conn { return c.cc } + +func (c *drpcOpenAIServiceClient) ChatCompletions(ctx context.Context, in *JSONPayload) (DRPCOpenAIService_ChatCompletionsClient, error) { + stream, err := c.cc.NewStream(ctx, "/aibridged.OpenAIService/ChatCompletions", drpcEncoding_File_aibridged_proto_aibridged_proto{}) + if err != nil { + return nil, err + } + x := &drpcOpenAIService_ChatCompletionsClient{stream} + if err := x.MsgSend(in, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { + return nil, err + } + if err := x.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type DRPCOpenAIService_ChatCompletionsClient interface { + drpc.Stream + Recv() (*JSONPayload, error) +} + +type drpcOpenAIService_ChatCompletionsClient struct { + drpc.Stream +} + +func (x *drpcOpenAIService_ChatCompletionsClient) GetStream() drpc.Stream { + return x.Stream +} + +func (x *drpcOpenAIService_ChatCompletionsClient) Recv() (*JSONPayload, error) { + m := new(JSONPayload) + if err := x.MsgRecv(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { + return nil, err + } + return m, nil +} + +func (x *drpcOpenAIService_ChatCompletionsClient) RecvMsg(m *JSONPayload) error { + return x.MsgRecv(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}) +} + +type DRPCOpenAIServiceServer interface { + ChatCompletions(*JSONPayload, DRPCOpenAIService_ChatCompletionsStream) error +} + +type DRPCOpenAIServiceUnimplementedServer struct{} + +func (s *DRPCOpenAIServiceUnimplementedServer) ChatCompletions(*JSONPayload, DRPCOpenAIService_ChatCompletionsStream) error { + return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +type DRPCOpenAIServiceDescription struct{} + +func (DRPCOpenAIServiceDescription) NumMethods() int { return 1 } + +func (DRPCOpenAIServiceDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { + switch n { + case 0: + return "/aibridged.OpenAIService/ChatCompletions", drpcEncoding_File_aibridged_proto_aibridged_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return nil, srv.(DRPCOpenAIServiceServer). + ChatCompletions( + in1.(*JSONPayload), + &drpcOpenAIService_ChatCompletionsStream{in2.(drpc.Stream)}, + ) + }, DRPCOpenAIServiceServer.ChatCompletions, true + default: + return "", nil, nil, nil, false + } +} + +func DRPCRegisterOpenAIService(mux drpc.Mux, impl DRPCOpenAIServiceServer) error { + return mux.Register(impl, DRPCOpenAIServiceDescription{}) +} + +type DRPCOpenAIService_ChatCompletionsStream interface { + drpc.Stream + Send(*JSONPayload) error +} + +type drpcOpenAIService_ChatCompletionsStream struct { + drpc.Stream +} + +func (x *drpcOpenAIService_ChatCompletionsStream) Send(m *JSONPayload) error { + return x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}) +} + +type DRPCAnthropicServiceClient interface { + DRPCConn() drpc.Conn + + Messages(ctx context.Context, in *JSONPayload) (DRPCAnthropicService_MessagesClient, error) +} + +type drpcAnthropicServiceClient struct { + cc drpc.Conn +} + +func NewDRPCAnthropicServiceClient(cc drpc.Conn) DRPCAnthropicServiceClient { + return &drpcAnthropicServiceClient{cc} +} + +func (c *drpcAnthropicServiceClient) DRPCConn() drpc.Conn { return c.cc } + +func (c *drpcAnthropicServiceClient) Messages(ctx context.Context, in *JSONPayload) (DRPCAnthropicService_MessagesClient, error) { + stream, err := c.cc.NewStream(ctx, "/aibridged.AnthropicService/Messages", drpcEncoding_File_aibridged_proto_aibridged_proto{}) + if err != nil { + return nil, err + } + x := &drpcAnthropicService_MessagesClient{stream} + if err := x.MsgSend(in, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { + return nil, err + } + if err := x.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type DRPCAnthropicService_MessagesClient interface { + drpc.Stream + Recv() (*JSONPayload, error) +} + +type drpcAnthropicService_MessagesClient struct { + drpc.Stream +} + +func (x *drpcAnthropicService_MessagesClient) GetStream() drpc.Stream { + return x.Stream +} + +func (x *drpcAnthropicService_MessagesClient) Recv() (*JSONPayload, error) { + m := new(JSONPayload) + if err := x.MsgRecv(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { + return nil, err + } + return m, nil +} + +func (x *drpcAnthropicService_MessagesClient) RecvMsg(m *JSONPayload) error { + return x.MsgRecv(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}) +} + +type DRPCAnthropicServiceServer interface { + Messages(*JSONPayload, DRPCAnthropicService_MessagesStream) error +} + +type DRPCAnthropicServiceUnimplementedServer struct{} + +func (s *DRPCAnthropicServiceUnimplementedServer) Messages(*JSONPayload, DRPCAnthropicService_MessagesStream) error { + return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +type DRPCAnthropicServiceDescription struct{} + +func (DRPCAnthropicServiceDescription) NumMethods() int { return 1 } + +func (DRPCAnthropicServiceDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { + switch n { + case 0: + return "/aibridged.AnthropicService/Messages", drpcEncoding_File_aibridged_proto_aibridged_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return nil, srv.(DRPCAnthropicServiceServer). + Messages( + in1.(*JSONPayload), + &drpcAnthropicService_MessagesStream{in2.(drpc.Stream)}, + ) + }, DRPCAnthropicServiceServer.Messages, true + default: + return "", nil, nil, nil, false + } +} + +func DRPCRegisterAnthropicService(mux drpc.Mux, impl DRPCAnthropicServiceServer) error { + return mux.Register(impl, DRPCAnthropicServiceDescription{}) +} + +type DRPCAnthropicService_MessagesStream interface { + drpc.Stream + Send(*JSONPayload) error +} + +type drpcAnthropicService_MessagesStream struct { + drpc.Stream +} + +func (x *drpcAnthropicService_MessagesStream) Send(m *JSONPayload) error { + return x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}) +} diff --git a/cli/server.go b/cli/server.go index 62b430cf22781..4786961d9b9fb 100644 --- a/cli/server.go +++ b/cli/server.go @@ -61,6 +61,8 @@ import ( "github.com/coder/serpent" "github.com/coder/wgtunnel/tunnelsdk" + "github.com/coder/coder/v2/aibridged" + aibridgedproto "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/coderd/ai" "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/notifications/reports" @@ -1049,6 +1051,36 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } provisionerdMetrics.Runner.NumDaemons.Set(float64(len(provisionerDaemons))) + // Since errCh only has one buffered slot, all routines + // sending on it must be wrapped in a select/default to + // avoid leaving dangling goroutines waiting for the + // channel to be consumed. + errCh = make(chan error, 1) + aiBridgeDaemons := make([]*aibridged.Server, 0) + defer func() { + // We have no graceful shutdown of aiBridgeDaemons + // here because that's handled at the end of main, this + // is here in case the program exits early. + for _, daemon := range aiBridgeDaemons { + _ = daemon.Close() + } + }() + + // Built in aibridge daemons. + for i := int64(0); i < vals.AI.Value.BridgeConfig.Daemons.Value(); i++ { + suffix := fmt.Sprintf("%d", i) + // The suffix is added to the hostname, so we may need to trim to fit into + // the 64 character limit. + hostname := stringutil.Truncate(cliutil.Hostname(), 63-len(suffix)) + name := fmt.Sprintf("%s-%s", hostname, suffix) + daemon, err := newAIBridgeDaemon(ctx, coderAPI, name) + if err != nil { + return xerrors.Errorf("create provisioner daemon: %w", err) + } + aiBridgeDaemons = append(aiBridgeDaemons, daemon) + } + coderAPI.AIBridgeDaemons = aiBridgeDaemons + shutdownConnsCtx, shutdownConns := context.WithCancel(ctx) defer shutdownConns() @@ -1232,6 +1264,35 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } wg.Wait() + + // Shut down aibridge daemons before waiting for WebSockets + // connections to close. + for i, aiBridgeDaemon := range aiBridgeDaemons { + id := i + 1 + wg.Add(1) + go func() { + defer wg.Done() + + r.Verbosef(inv, "Shutting down AI bridge daemon %d...", id) + + err := shutdownWithTimeout(func(ctx context.Context) error { + // We only want to cancel active jobs if we aren't exiting gracefully. + return aiBridgeDaemon.Shutdown(ctx) + }, 5 * time.Second) + if err != nil { + cliui.Errorf(inv.Stderr, "Failed to shut down AI bridge daemon %d: %s\n", id, err) + return + } + err = aiBridgeDaemon.Close() + if err != nil { + cliui.Errorf(inv.Stderr, "Close AI bridge daemon %d: %s\n", id, err) + return + } + r.Verbosef(inv, "Gracefully shut down AI bridge daemon %d", id) + }() + } + wg.Wait() + cliui.Info(inv.Stdout, "Waiting for WebSocket connections to close..."+"\n") _ = coderAPICloser.Close() cliui.Info(inv.Stdout, "Done waiting for WebSocket connections"+"\n") @@ -1532,6 +1593,14 @@ func newProvisionerDaemon( }), nil } +func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string) (*aibridged.Server, error) { + return aibridged.New(func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { + // This debounces calls to listen every second. + // TODO: is this true / necessary? + return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx, name) + }, coderAPI.Logger.Named("aibridged").With(slog.F("name", name))) +} + // nolint: revive func PrintLogo(inv *serpent.Invocation, daemonTitle string) { // Only print the logo in TTYs. diff --git a/coderd/aibridge.go b/coderd/aibridge.go new file mode 100644 index 0000000000000..79362232a6216 --- /dev/null +++ b/coderd/aibridge.go @@ -0,0 +1,57 @@ +package coderd + +import ( + "net/http" + "time" + + "cdr.dev/slog" + + "github.com/coder/coder/v2/aibridged" + "github.com/coder/coder/v2/coderd/util/slice" +) + +func (api *API) bridgeOpenAIRequest(rw http.ResponseWriter, r *http.Request) { + api.bridgeAIRequest(rw, r, aibridged.AIProviderOpenAI) +} + +func (api *API) bridgeAnthropicRequest(rw http.ResponseWriter, r *http.Request) { + api.bridgeAIRequest(rw, r, aibridged.AIProviderAnthropic) +} + +func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request, client any) { + ctx := r.Context() + + if len(api.AIBridgeDaemons) == 0 { + http.Error(rw, "no AI bridge daemons running", http.StatusInternalServerError) + return + } + + server, err := slice.PickRandom(api.AIBridgeDaemons) + if err != nil { + api.Logger.Error(ctx, "failed to pick random AI bridge server", slog.Error(err)) + http.Error(rw, "failed to select AI bridge", http.StatusInternalServerError) + return + } + + // TODO: use same context or new? + c, err := api.CreateInMemoryOpenAIBridgeClient(ctx, server) + if err != nil { + api.Logger.Error(ctx, "failed to create OpenAI bridge", slog.Error(err)) + http.Error(rw, "failed to create OpenAI bridge", http.StatusInternalServerError) + return + } + + // TODO: don't create a new proxy on each request. + proxy, err := aibridged.NewDRPCProxy(aibridged.NewOpenAIAdapter(c), aibridged.ProxyConfig{ + ReadTimeout: time.Second * 60, // TODO: read timeout. + + }) + + if err != nil { + api.Logger.Error(ctx, "failed to proxy HTTP request to AI bridge daemon", slog.Error(err)) + http.Error(rw, "failed to proxy HTTP request to AI bridge", http.StatusInternalServerError) + return + } + + http.StripPrefix("/api/v2/aibridge", proxy).ServeHTTP(rw, r) +} diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go new file mode 100644 index 0000000000000..a9796f7532eff --- /dev/null +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -0,0 +1,24 @@ +package aibridgedserver + +import ( + "context" + + "github.com/coder/coder/v2/aibridged/proto" +) + +type Server struct { + // lifecycleCtx must be tied to the API server's lifecycle + // as when the API server shuts down, we want to cancel any + // long-running operations. + lifecycleCtx context.Context +} + +func (s *Server) AuditPrompt(_ context.Context, req *proto.AuditPromptRequest) (*proto.AuditPromptResponse, error) { + return &proto.AuditPromptResponse{}, nil +} + +func NewServer(lifecycleCtx context.Context) (*Server, error) { + return &Server{ + lifecycleCtx: lifecycleCtx, + }, nil +} diff --git a/coderd/coderd.go b/coderd/coderd.go index 69d942304acea..d317dc5563f55 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -19,8 +19,6 @@ import ( "sync/atomic" "time" - "github.com/coder/coder/v2/coderd/prebuilds" - "github.com/andybalholm/brotli" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -39,6 +37,11 @@ import ( "tailscale.com/types/key" "tailscale.com/util/singleflight" + "github.com/coder/coder/v2/aibridged" + "github.com/coder/coder/v2/coderd/aibridgedserver" + "github.com/coder/coder/v2/coderd/prebuilds" + provisionerdproto "github.com/coder/coder/v2/provisionerd/proto" + "cdr.dev/slog" "github.com/coder/quartz" "github.com/coder/serpent" @@ -54,6 +57,7 @@ import ( "github.com/coder/coder/v2/coderd/webpush" agentproto "github.com/coder/coder/v2/agent/proto" + aibridgedproto "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/buildinfo" _ "github.com/coder/coder/v2/coderd/apidoc" // Used for swagger docs. "github.com/coder/coder/v2/coderd/appearance" @@ -88,7 +92,6 @@ import ( "github.com/coder/coder/v2/coderd/workspacestats" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/healthsdk" - "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/site" "github.com/coder/coder/v2/tailnet" @@ -606,7 +609,7 @@ func New(options *Options) *API { ExternalURL: buildinfo.ExternalURL(), Version: buildinfo.Version(), AgentAPIVersion: AgentAPIVersionREST, - ProvisionerAPIVersion: proto.CurrentVersion.String(), + ProvisionerAPIVersion: provisionerdproto.CurrentVersion.String(), DashboardURL: api.AccessURL.String(), WorkspaceProxy: false, UpgradeMessage: api.DeploymentValues.CLIUpgradeMessage.String(), @@ -667,7 +670,7 @@ func New(options *Options) *API { }, ProvisionerDaemons: healthcheck.ProvisionerDaemonsReportDeps{ CurrentVersion: buildinfo.Version(), - CurrentAPIMajorVersion: proto.CurrentMajor, + CurrentAPIMajorVersion: provisionerdproto.CurrentMajor, Store: options.Database, StaleInterval: provisionerdserver.StaleInterval, // TimeNow set to default, see healthcheck/provisioner.go @@ -1491,6 +1494,10 @@ func New(options *Options) *API { r.Use(apiKeyMiddleware) r.Get("/", api.tailnetRPCConn) }) + r.Route("/aibridge", func(r chi.Router) { + r.Post("/v1/chat/completions", api.bridgeOpenAIRequest) + r.Post("/v1/messages", api.bridgeAnthropicRequest) + }) }) if options.SwaggerEndpoint { @@ -1578,7 +1585,7 @@ type API struct { TailnetClientService *tailnet.ClientService // WebpushDispatcher is a way to send notifications to users via Web Push. WebpushDispatcher webpush.Dispatcher - QuotaCommitter atomic.Pointer[proto.QuotaCommitter] + QuotaCommitter atomic.Pointer[provisionerdproto.QuotaCommitter] AppearanceFetcher atomic.Pointer[appearance.Fetcher] // WorkspaceProxyHostsFn returns the hosts of healthy workspace proxies // for header reasons. @@ -1634,6 +1641,8 @@ type API struct { // dbRolluper rolls up template usage stats from raw agent and app // stats. This is used to provide insights in the WebUI. dbRolluper *dbrollup.Rolluper + + AIBridgeDaemons []*aibridged.Server } // Close waits for all WebSocket connections to drain before returning. @@ -1734,11 +1743,11 @@ type memoryProvisionerDaemonOptions struct { // CreateInMemoryProvisionerDaemon is an in-memory connection to a provisionerd. // Useful when starting coderd and provisionerd in the same process. -func (api *API) CreateInMemoryProvisionerDaemon(dialCtx context.Context, name string, provisionerTypes []codersdk.ProvisionerType) (client proto.DRPCProvisionerDaemonClient, err error) { +func (api *API) CreateInMemoryProvisionerDaemon(dialCtx context.Context, name string, provisionerTypes []codersdk.ProvisionerType) (client provisionerdproto.DRPCProvisionerDaemonClient, err error) { return api.CreateInMemoryTaggedProvisionerDaemon(dialCtx, name, provisionerTypes, nil) } -func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, name string, provisionerTypes []codersdk.ProvisionerType, provisionerTags map[string]string, opts ...MemoryProvisionerDaemonOption) (client proto.DRPCProvisionerDaemonClient, err error) { +func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, name string, provisionerTypes []codersdk.ProvisionerType, provisionerTags map[string]string, opts ...MemoryProvisionerDaemonOption) (client provisionerdproto.DRPCProvisionerDaemonClient, err error) { options := &memoryProvisionerDaemonOptions{} for _, opt := range opts { opt(options) @@ -1770,7 +1779,7 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n return nil, xerrors.Errorf("failed to parse built-in provisioner key ID: %w", err) } - apiVersion := proto.CurrentVersion.String() + apiVersion := provisionerdproto.CurrentVersion.String() if options.versionOverride != "" && flag.Lookup("test.v") != nil { // This should only be usable for unit testing. To fake a different provisioner version apiVersion = options.versionOverride @@ -1825,7 +1834,7 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n if err != nil { return nil, err } - err = proto.DRPCRegisterProvisionerDaemon(mux, srv) + err = provisionerdproto.DRPCRegisterProvisionerDaemon(mux, srv) if err != nil { return nil, err } @@ -1860,7 +1869,119 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n _ = serverSession.Close() }() - return proto.NewDRPCProvisionerDaemonClient(clientSession), nil + return provisionerdproto.NewDRPCProvisionerDaemonClient(clientSession), nil +} + +func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name string) (client aibridgedproto.DRPCAIBridgeDaemonClient, err error) { + // TODO(dannyk): implement options. + // TODO(dannyk): implement tracing. + + clientSession, serverSession := drpcsdk.MemTransportPipe() + defer func() { + if err != nil { + _ = clientSession.Close() + _ = serverSession.Close() + } + }() + + // TODO(dannyk): implement API versioning. + // TODO(dannyk): implement database tracking of daemons. + + mux := drpcmux.New() + api.Logger.Debug(dialCtx, "starting in-memory AI bridge daemon", slog.F("name", name)) + logger := api.Logger.Named(fmt.Sprintf("inmem-aibridged-%s", name)) + srv, err := aibridgedserver.NewServer(api.ctx) + if err != nil { + return nil, err + } + err = aibridgedproto.DRPCRegisterAIBridgeDaemon(mux, srv) + if err != nil { + return nil, err + } + server := drpcserver.NewWithOptions(&tracing.DRPCHandler{Handler: mux}, + drpcserver.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), + Log: func(err error) { + if xerrors.Is(err, io.EOF) { + return + } + logger.Debug(dialCtx, "drpc server error", slog.Error(err)) + }, + }, + ) + // in-mem pipes aren't technically "websockets" but they have the same properties as far as the + // API is concerned: they are long-lived connections that we need to close before completing + // shutdown of the API. + api.WebsocketWaitMutex.Lock() + api.WebsocketWaitGroup.Add(1) + api.WebsocketWaitMutex.Unlock() + go func() { + defer api.WebsocketWaitGroup.Done() + // Here we pass the background context, since we want the server to keep serving until the + // client hangs up. The aibridged is local, in-mem, so there isn't a danger of losing contact with it and + // having a dead connection we don't know the status of. + err := server.Serve(context.Background(), serverSession) + logger.Info(dialCtx, "AI bridge daemon disconnected", slog.Error(err)) + // close the sessions, so we don't leak goroutines serving them. + _ = clientSession.Close() + _ = serverSession.Close() + }() + + return aibridgedproto.NewDRPCAIBridgeDaemonClient(clientSession), nil +} + +// TODO: naming... +func (api *API) CreateInMemoryOpenAIBridgeClient(dialCtx context.Context, srv *aibridged.Server) (client aibridgedproto.DRPCOpenAIServiceClient, err error) { + // TODO(dannyk): implement options. + // TODO(dannyk): implement tracing. + + clientSession, serverSession := drpcsdk.MemTransportPipe() + defer func() { + if err != nil { + _ = clientSession.Close() + _ = serverSession.Close() + } + }() + + // TODO(dannyk): implement API versioning. + + mux := drpcmux.New() + api.Logger.Debug(dialCtx, "starting in-memory OpenAI bridge") + logger := api.Logger.Named("inmem-openai-bridge") + err = aibridgedproto.DRPCRegisterOpenAIService(mux, srv) + if err != nil { + return nil, err + } + server := drpcserver.NewWithOptions(&tracing.DRPCHandler{Handler: mux}, + drpcserver.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), + Log: func(err error) { + if xerrors.Is(err, io.EOF) { + return + } + logger.Debug(dialCtx, "drpc server error", slog.Error(err)) + }, + }, + ) + // in-mem pipes aren't technically "websockets" but they have the same properties as far as the + // API is concerned: they are long-lived connections that we need to close before completing + // shutdown of the API. + api.WebsocketWaitMutex.Lock() + api.WebsocketWaitGroup.Add(1) + api.WebsocketWaitMutex.Unlock() + go func() { + defer api.WebsocketWaitGroup.Done() + // Here we pass the background context, since we want the server to keep serving until the + // client hangs up. The aibridged is local, in-mem, so there isn't a danger of losing contact with it and + // having a dead connection we don't know the status of. + err := server.Serve(context.Background(), serverSession) + logger.Info(dialCtx, "OpenAI bridge disconnected", slog.Error(err)) + // close the sessions, so we don't leak goroutines serving them. + _ = clientSession.Close() + _ = serverSession.Close() + }() + + return aibridgedproto.NewDRPCOpenAIServiceClient(clientSession), nil } func (api *API) DERPMap() *tailcfg.DERPMap { diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index f3811650786b7..8e54c1bcd093c 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -1,7 +1,10 @@ package slice import ( + "math/rand" + "golang.org/x/exp/constraints" + "golang.org/x/xerrors" ) // ToStrings works for any type where the base type is a string. @@ -90,6 +93,25 @@ func Find[T any](haystack []T, cond func(T) bool) (T, bool) { return empty, false } +// PickRandom returns one element at a random index. +// NOTE: callers MUST protect haystack from modification while this function is called to prevent panics. +func PickRandom[T any](haystack []T) (T, error) { + var zero T + + length := len(haystack) + if length == 0 { + return zero, xerrors.New("cannot pick from empty slice") + } + + index := rand.Intn(length) + + if length != len(haystack) { + return zero, xerrors.New("slice was modified during operation") + } + + return haystack[index], nil +} + // Filter returns all elements that satisfy the condition. func Filter[T any](haystack []T, cond func(T) bool) []T { out := make([]T, 0, len(haystack)) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 696e6bda52682..ecfb589c5ad47 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1055,6 +1055,10 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Parent: &deploymentGroupNotifications, YAML: "inbox", } + deploymentGroupAIBridge = serpent.Group{ + Name: "AI Bridge", + YAML: "ai_bridge", + } ) httpAddress := serpent.Option{ @@ -3103,6 +3107,19 @@ Write out the current server config as YAML to stdout.`, YAML: "failure_hard_limit", Hidden: true, }, + + // AI Bridge Options + { + Name: "AI Bridge Daemons", + Description: "TODO.", + Flag: "ai-bridge-daemons", + Env: "CODER_AI_BRIDGE_DAEMONS", + Value: &c.AI.Value.BridgeConfig.Daemons, + Default: "3", + Group: &deploymentGroupAIBridge, + YAML: "daemons", + Hidden: true, + }, } return opts @@ -3121,6 +3138,10 @@ type AIProviderConfig struct { type AIConfig struct { Providers []AIProviderConfig `json:"providers,omitempty" yaml:"providers,omitempty"` + + BridgeConfig struct { + Daemons serpent.Int64 `json:"daemons" typescript:",notnull"` + } `json:"bridge,omitempty"` } type SupportConfig struct { From cada69e6bf3f56ef47978f070c907be13c470cfd Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 4 Jun 2025 11:26:26 +0200 Subject: [PATCH 02/61] WIP: replace dRPC API with HTTP API Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 127 ++++++++------- aibridged/bridge.go | 55 +++++++ aibridged/http_proxy.go | 232 +++++++++++++-------------- aibridged/interface.go | 39 ----- aibridged/proto/aibridged.pb.go | 111 ++----------- aibridged/proto/aibridged.proto | 16 -- aibridged/proto/aibridged_drpc.pb.go | 206 ------------------------ cli/server.go | 4 +- coderd/aibridge.go | 40 ++--- coderd/coderd.go | 110 ++++++------- 10 files changed, 330 insertions(+), 610 deletions(-) create mode 100644 aibridged/bridge.go delete mode 100644 aibridged/interface.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index bbf0ef640a149..51a02321df24c 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -47,21 +47,36 @@ type Server struct { shuttingDownB bool // shuttingDownCh will receive when we start graceful shutdown shuttingDownCh chan struct{} + + bridge *Bridge } -func New(clientDialer Dialer, logger slog.Logger) (*Server, error) { - ctx, ctxCancel := context.WithCancel(context.Background()) +func New(rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) { + if rpcDialer == nil { + return nil, xerrors.Errorf("nil rpcDialer given") + } + + ctx, cancel := context.WithCancel(context.Background()) + bridge := NewBridge(httpAddr) daemon := &Server{ logger: logger, - clientDialer: clientDialer, + clientDialer: rpcDialer, clientCh: make(chan proto.DRPCAIBridgeDaemonClient), closeContext: ctx, - closeCancel: ctxCancel, + closeCancel: cancel, closedCh: make(chan struct{}), shuttingDownCh: make(chan struct{}), initConnectionCh: make(chan struct{}), + + bridge: bridge, } go daemon.connect() + go func() { + err := bridge.Serve() + // TODO: better error handling. + // TODO: close on shutdown. + logger.Error(ctx, "bridge server stopped", slog.Error(err)) + }() return daemon, nil } // Connect establishes a connection to coderd. @@ -148,57 +163,61 @@ func (s *Server) AuditPrompt(ctx context.Context, in *proto.AuditPromptRequest) return out, nil } -func (s *Server) ChatCompletions(payload *proto.JSONPayload, stream proto.DRPCOpenAIService_ChatCompletionsStream) error { - // TODO: call OpenAI API. +//func (s *Server) ChatCompletions(payload *proto.JSONPayload, stream proto.DRPCOpenAIService_ChatCompletionsStream) error { +// // TODO: call OpenAI API. +// +// select { +// case <-stream.Context().Done(): +// return nil +// default: +// } +// +// err := stream.Send(&proto.JSONPayload{ +// Content: ` +//{ +// "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT", +// "object": "chat.completion", +// "created": 1741569952, +// "model": "gpt-4.1-2025-04-14", +// "choices": [ +// { +// "index": 0, +// "message": { +// "role": "assistant", +// "content": "Hello! How can I assist you today?", +// "refusal": null, +// "annotations": [] +// }, +// "logprobs": null, +// "finish_reason": "stop" +// } +// ], +// "usage": { +// "prompt_tokens": 19, +// "completion_tokens": 10, +// "total_tokens": 29, +// "prompt_tokens_details": { +// "cached_tokens": 0, +// "audio_tokens": 0 +// }, +// "completion_tokens_details": { +// "reasoning_tokens": 0, +// "audio_tokens": 0, +// "accepted_prediction_tokens": 0, +// "rejected_prediction_tokens": 0 +// } +// }, +// "service_tier": "default" +//} +//`}) +// if err != nil { +// return xerrors.Errorf("stream chat completion response: %w", err) +// } +// return nil +//} - select { - case <-stream.Context().Done(): - return nil - default: - } - - err := stream.Send(&proto.JSONPayload{ - Content: ` -{ - "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT", - "object": "chat.completion", - "created": 1741569952, - "model": "gpt-4.1-2025-04-14", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?", - "refusal": null, - "annotations": [] - }, - "logprobs": null, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 19, - "completion_tokens": 10, - "total_tokens": 29, - "prompt_tokens_details": { - "cached_tokens": 0, - "audio_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0, - "audio_tokens": 0, - "accepted_prediction_tokens": 0, - "rejected_prediction_tokens": 0 - } - }, - "service_tier": "default" -} -`}) - if err != nil { - return xerrors.Errorf("stream chat completion response: %w", err) - } - return nil +func (s *Server) BridgeAddr() string { + return s.bridge.Addr() } // TODO: direct copy/paste from provisionerd, abstract into common util. diff --git a/aibridged/bridge.go b/aibridged/bridge.go new file mode 100644 index 0000000000000..c95db9f654041 --- /dev/null +++ b/aibridged/bridge.go @@ -0,0 +1,55 @@ +package aibridged + +import ( + "fmt" + "net" + "net/http" + + "golang.org/x/xerrors" +) + +type Bridge struct { + srv *http.Server + addr string +} + +func NewBridge(addr string) *Bridge { + mux := &http.ServeMux{} + mux.HandleFunc("/v1/chat/completions", bridgeOpenAIRequest) + mux.HandleFunc("/v1/messages", bridgeAnthropicRequest) + + srv := &http.Server{ + Addr: addr, + Handler: mux, + // TODO: other settings. + } + + return &Bridge{srv: srv} +} + +func (b *Bridge) Serve() error { + list, err := net.Listen("tcp", b.srv.Addr) + if err != nil { + return xerrors.Errorf("listen: %w", err) + } + + b.addr = list.Addr().String() + + return b.srv.Serve(list) // TODO: TLS. +} + +func (b *Bridge) Addr() string { + return b.addr +} + +func bridgeOpenAIRequest(w http.ResponseWriter, r *http.Request) { + fmt.Println("OpenAI") +} + +func bridgeAnthropicRequest(w http.ResponseWriter, r *http.Request) { + fmt.Println("Anthropic") +} + +func bridgeRequest(w http.ResponseWriter, r *http.Request) { + +} diff --git a/aibridged/http_proxy.go b/aibridged/http_proxy.go index c0b3e8f0dfcb2..eb41821d06dc4 100644 --- a/aibridged/http_proxy.go +++ b/aibridged/http_proxy.go @@ -1,118 +1,118 @@ package aibridged -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "cdr.dev/slog" - "storj.io/drpc/drpcmux" - - "github.com/coder/coder/v2/aibridged/proto" -) - -type AIProvider string - -const ( - AIProviderOpenAI AIProvider = "openai" - AIProviderAnthropic AIProvider = "anthropic" -) - -type ProxyConfig struct { - ReadTimeout time.Duration -} - -type HTTPProxy struct { - logger slog.Logger - provider AIProvider - config ProxyConfig - mux *drpcmux.Mux - drpcClient AIServiceClient -} - -// NewDRPCProxy creates a new reverse proxy instance. -func NewDRPCProxy(client AIServiceClient, config ProxyConfig) (*HTTPProxy, error) { - return &HTTPProxy{ - config: config, - mux: drpcmux.New(), - drpcClient: client, - }, nil -} // ServeHTTP handles incoming HTTP requests and proxies them to dRPC - -func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Read request body - body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MiB // TODO: make configurable. - if err != nil { - http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest) - return - } - defer r.Body.Close() - - // TODO: use body - _ = body - - // Create context with timeout - ctx, cancel := context.WithTimeout(r.Context(), p.config.ReadTimeout) - defer cancel() - - switch p.drpcClient.(type) { - case *OpenAIAdapter: - payload := struct { - messages []struct { - Role string `json:"role,omitempty"` - Content string `json:"content,omitempty"` - } - model string - }{ - messages: []struct { - Role string `json:"role,omitempty"` - Content string `json:"content,omitempty"` - }{ - { - Role: "developer", - Content: "what is 9/0?", - }, - }, - model: "gpt-4o", - } - payloadJSON, err := json.Marshal(payload) - if err != nil { - // TODO: better error. - http.Error(w, "failed to marshal payload", http.StatusInternalServerError) - return - } - - resp, err := p.drpcClient.SendRequest(ctx, &proto.JSONPayload{ - Content: string(payloadJSON), - }) - if err != nil { - return - } - - // TODO: start SSE - for { - respPayload, err := resp.Recv() - if err != nil { - if err == io.EOF { - return - } - http.Error(w, "failed to receive payload", http.StatusInternalServerError) - return - } - - _, _ = w.Write([]byte(respPayload.Content)) - } - - // Set appropriate headers - //w.Header().Set("Content-Type", "application/json") - - // Write response - //if _, err := w.Write([]byte(resp.)); err != nil { - // p.logger.Warn(ctx, "failed to write response: %w", err) - //} - case *AnthropicAdapter: - } -} +//import ( +// "context" +// "encoding/json" +// "fmt" +// "io" +// "net/http" +// "time" +// +// "cdr.dev/slog" +// "storj.io/drpc/drpcmux" +// +// "github.com/coder/coder/v2/aibridged/proto" +//) +// +//type AIProvider string +// +//const ( +// AIProviderOpenAI AIProvider = "openai" +// AIProviderAnthropic AIProvider = "anthropic" +//) +// +//type ProxyConfig struct { +// ReadTimeout time.Duration +//} +// +//type HTTPProxy struct { +// logger slog.Logger +// provider AIProvider +// config ProxyConfig +// mux *drpcmux.Mux +// drpcClient AIServiceClient +//} +// +//// NewDRPCProxy creates a new reverse proxy instance. +//func NewDRPCProxy(client AIServiceClient, config ProxyConfig) (*HTTPProxy, error) { +// return &HTTPProxy{ +// config: config, +// mux: drpcmux.New(), +// drpcClient: client, +// }, nil +//} // ServeHTTP handles incoming HTTP requests and proxies them to dRPC +// +//func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// // Read request body +// body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MiB // TODO: make configurable. +// if err != nil { +// http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest) +// return +// } +// defer r.Body.Close() +// +// // TODO: use body +// _ = body +// +// // Create context with timeout +// ctx, cancel := context.WithTimeout(r.Context(), p.config.ReadTimeout) +// defer cancel() +// +// switch p.drpcClient.(type) { +// case *OpenAIAdapter: +// payload := struct { +// messages []struct { +// Role string `json:"role,omitempty"` +// Content string `json:"content,omitempty"` +// } +// model string +// }{ +// messages: []struct { +// Role string `json:"role,omitempty"` +// Content string `json:"content,omitempty"` +// }{ +// { +// Role: "developer", +// Content: "what is 9/0?", +// }, +// }, +// model: "gpt-4o", +// } +// payloadJSON, err := json.Marshal(payload) +// if err != nil { +// // TODO: better error. +// http.Error(w, "failed to marshal payload", http.StatusInternalServerError) +// return +// } +// +// resp, err := p.drpcClient.SendRequest(ctx, &proto.JSONPayload{ +// Content: string(payloadJSON), +// }) +// if err != nil { +// return +// } +// +// // TODO: start SSE +// for { +// respPayload, err := resp.Recv() +// if err != nil { +// if err == io.EOF { +// return +// } +// http.Error(w, "failed to receive payload", http.StatusInternalServerError) +// return +// } +// +// _, _ = w.Write([]byte(respPayload.Content)) +// } +// +// // Set appropriate headers +// //w.Header().Set("Content-Type", "application/json") +// +// // Write response +// //if _, err := w.Write([]byte(resp.)); err != nil { +// // p.logger.Warn(ctx, "failed to write response: %w", err) +// //} +// case *AnthropicAdapter: +// } +//} diff --git a/aibridged/interface.go b/aibridged/interface.go deleted file mode 100644 index 665952d743f37..0000000000000 --- a/aibridged/interface.go +++ /dev/null @@ -1,39 +0,0 @@ -package aibridged - -import ( - "context" - - "storj.io/drpc" - - "github.com/coder/coder/v2/aibridged/proto" -) - -// Define a common interface for AI services -type AIServiceClient interface { - SendRequest(ctx context.Context, payload *proto.JSONPayload) (StreamingResponder[*proto.JSONPayload], error) -} - -type StreamingResponder[T any] interface { - drpc.Stream - Recv() (T, error) -} - -type OpenAIAdapter struct { - client proto.DRPCOpenAIServiceClient -} - -func NewOpenAIAdapter(client proto.DRPCOpenAIServiceClient) *OpenAIAdapter { - return &OpenAIAdapter{client: client} -} - -func (a *OpenAIAdapter) SendRequest(ctx context.Context, payload *proto.JSONPayload) (StreamingResponder[*proto.JSONPayload], error) { - return a.client.ChatCompletions(ctx, payload) -} - -type AnthropicAdapter struct { - client proto.DRPCAnthropicServiceClient -} - -func (a *AnthropicAdapter) SendRequest(ctx context.Context, payload *proto.JSONPayload) (StreamingResponder[*proto.JSONPayload], error) { - return a.client.Messages(ctx, payload) -} diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index 0bc64f0bf7b86..addcb6c0f7886 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -113,53 +113,6 @@ func (*AuditPromptResponse) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} } -type JSONPayload struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` // Rather than modelling the full API in proto, let's just accept the JSON payload and naïvely pass it along. -} - -func (x *JSONPayload) Reset() { - *x = JSONPayload{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *JSONPayload) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*JSONPayload) ProtoMessage() {} - -func (x *JSONPayload) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use JSONPayload.ProtoReflect.Descriptor instead. -func (*JSONPayload) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} -} - -func (x *JSONPayload) GetContent() string { - if x != nil { - return x.Content - } - return "" -} - var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ @@ -171,30 +124,17 @@ var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x13, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x0a, - 0x0b, 0x4a, 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x18, 0x0a, 0x07, - 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x32, 0x5e, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, - 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, - 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1d, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, - 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, - 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x54, 0x0a, 0x0d, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x0f, 0x43, 0x68, 0x61, 0x74, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x69, 0x62, - 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, 0x6c, 0x6f, - 0x61, 0x64, 0x1a, 0x16, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x4a, - 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x30, 0x01, 0x32, 0x50, 0x0a, 0x10, - 0x41, 0x6e, 0x74, 0x68, 0x72, 0x6f, 0x70, 0x69, 0x63, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x3c, 0x0a, 0x08, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, - 0x6c, 0x6f, 0x61, 0x64, 0x1a, 0x16, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, - 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x30, 0x01, 0x42, 0x2b, - 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, - 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x5e, 0x0a, + 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, + 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1d, + 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, + 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, + 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, + 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, + 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, + 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -209,21 +149,16 @@ func file_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { return file_aibridged_proto_aibridged_proto_rawDescData } -var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*AuditPromptRequest)(nil), // 0: aibridged.AuditPromptRequest (*AuditPromptResponse)(nil), // 1: aibridged.AuditPromptResponse - (*JSONPayload)(nil), // 2: aibridged.JSONPayload } var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ 0, // 0: aibridged.AIBridgeDaemon.AuditPrompt:input_type -> aibridged.AuditPromptRequest - 2, // 1: aibridged.OpenAIService.ChatCompletions:input_type -> aibridged.JSONPayload - 2, // 2: aibridged.AnthropicService.Messages:input_type -> aibridged.JSONPayload - 1, // 3: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse - 2, // 4: aibridged.OpenAIService.ChatCompletions:output_type -> aibridged.JSONPayload - 2, // 5: aibridged.AnthropicService.Messages:output_type -> aibridged.JSONPayload - 3, // [3:6] is the sub-list for method output_type - 0, // [0:3] is the sub-list for method input_type + 1, // 1: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -259,18 +194,6 @@ func file_aibridged_proto_aibridged_proto_init() { return nil } } - file_aibridged_proto_aibridged_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*JSONPayload); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } } type x struct{} out := protoimpl.TypeBuilder{ @@ -278,9 +201,9 @@ func file_aibridged_proto_aibridged_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_aibridged_proto_aibridged_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 2, NumExtensions: 0, - NumServices: 3, + NumServices: 1, }, GoTypes: file_aibridged_proto_aibridged_proto_goTypes, DependencyIndexes: file_aibridged_proto_aibridged_proto_depIdxs, diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index 23e1540af1c34..b68f00fa108a1 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -17,19 +17,3 @@ service AIBridgeDaemon { // AuditPrompt allows aibridged to report back to coderd that a prompt was issued by a user to an AI provider. rpc AuditPrompt(AuditPromptRequest) returns (AuditPromptResponse); } - -message JSONPayload { - string content = 1; // Rather than modelling the full API in proto, let's just accept the JSON payload and naïvely pass it along. -} - -// OpenAIService describes the service exposed by aibridged, which allows coderd to proxy AI requests to it. -// TODO(dannyk): versioning of both upstream service and this service. -service OpenAIService { - rpc ChatCompletions(JSONPayload) returns (stream JSONPayload); -} - -// AnthropicService describes the service exposed by aibridged, which allows coderd to proxy AI requests to it. -// TODO(dannyk): versioning of both upstream service and this service. -service AnthropicService { - rpc Messages(JSONPayload) returns (stream JSONPayload); -} diff --git a/aibridged/proto/aibridged_drpc.pb.go b/aibridged/proto/aibridged_drpc.pb.go index 7aa99e182285b..d59200d81a885 100644 --- a/aibridged/proto/aibridged_drpc.pb.go +++ b/aibridged/proto/aibridged_drpc.pb.go @@ -109,209 +109,3 @@ func (x *drpcAIBridgeDaemon_AuditPromptStream) SendAndClose(m *AuditPromptRespon } return x.CloseSend() } - -type DRPCOpenAIServiceClient interface { - DRPCConn() drpc.Conn - - ChatCompletions(ctx context.Context, in *JSONPayload) (DRPCOpenAIService_ChatCompletionsClient, error) -} - -type drpcOpenAIServiceClient struct { - cc drpc.Conn -} - -func NewDRPCOpenAIServiceClient(cc drpc.Conn) DRPCOpenAIServiceClient { - return &drpcOpenAIServiceClient{cc} -} - -func (c *drpcOpenAIServiceClient) DRPCConn() drpc.Conn { return c.cc } - -func (c *drpcOpenAIServiceClient) ChatCompletions(ctx context.Context, in *JSONPayload) (DRPCOpenAIService_ChatCompletionsClient, error) { - stream, err := c.cc.NewStream(ctx, "/aibridged.OpenAIService/ChatCompletions", drpcEncoding_File_aibridged_proto_aibridged_proto{}) - if err != nil { - return nil, err - } - x := &drpcOpenAIService_ChatCompletionsClient{stream} - if err := x.MsgSend(in, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { - return nil, err - } - if err := x.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type DRPCOpenAIService_ChatCompletionsClient interface { - drpc.Stream - Recv() (*JSONPayload, error) -} - -type drpcOpenAIService_ChatCompletionsClient struct { - drpc.Stream -} - -func (x *drpcOpenAIService_ChatCompletionsClient) GetStream() drpc.Stream { - return x.Stream -} - -func (x *drpcOpenAIService_ChatCompletionsClient) Recv() (*JSONPayload, error) { - m := new(JSONPayload) - if err := x.MsgRecv(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { - return nil, err - } - return m, nil -} - -func (x *drpcOpenAIService_ChatCompletionsClient) RecvMsg(m *JSONPayload) error { - return x.MsgRecv(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}) -} - -type DRPCOpenAIServiceServer interface { - ChatCompletions(*JSONPayload, DRPCOpenAIService_ChatCompletionsStream) error -} - -type DRPCOpenAIServiceUnimplementedServer struct{} - -func (s *DRPCOpenAIServiceUnimplementedServer) ChatCompletions(*JSONPayload, DRPCOpenAIService_ChatCompletionsStream) error { - return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) -} - -type DRPCOpenAIServiceDescription struct{} - -func (DRPCOpenAIServiceDescription) NumMethods() int { return 1 } - -func (DRPCOpenAIServiceDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { - switch n { - case 0: - return "/aibridged.OpenAIService/ChatCompletions", drpcEncoding_File_aibridged_proto_aibridged_proto{}, - func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { - return nil, srv.(DRPCOpenAIServiceServer). - ChatCompletions( - in1.(*JSONPayload), - &drpcOpenAIService_ChatCompletionsStream{in2.(drpc.Stream)}, - ) - }, DRPCOpenAIServiceServer.ChatCompletions, true - default: - return "", nil, nil, nil, false - } -} - -func DRPCRegisterOpenAIService(mux drpc.Mux, impl DRPCOpenAIServiceServer) error { - return mux.Register(impl, DRPCOpenAIServiceDescription{}) -} - -type DRPCOpenAIService_ChatCompletionsStream interface { - drpc.Stream - Send(*JSONPayload) error -} - -type drpcOpenAIService_ChatCompletionsStream struct { - drpc.Stream -} - -func (x *drpcOpenAIService_ChatCompletionsStream) Send(m *JSONPayload) error { - return x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}) -} - -type DRPCAnthropicServiceClient interface { - DRPCConn() drpc.Conn - - Messages(ctx context.Context, in *JSONPayload) (DRPCAnthropicService_MessagesClient, error) -} - -type drpcAnthropicServiceClient struct { - cc drpc.Conn -} - -func NewDRPCAnthropicServiceClient(cc drpc.Conn) DRPCAnthropicServiceClient { - return &drpcAnthropicServiceClient{cc} -} - -func (c *drpcAnthropicServiceClient) DRPCConn() drpc.Conn { return c.cc } - -func (c *drpcAnthropicServiceClient) Messages(ctx context.Context, in *JSONPayload) (DRPCAnthropicService_MessagesClient, error) { - stream, err := c.cc.NewStream(ctx, "/aibridged.AnthropicService/Messages", drpcEncoding_File_aibridged_proto_aibridged_proto{}) - if err != nil { - return nil, err - } - x := &drpcAnthropicService_MessagesClient{stream} - if err := x.MsgSend(in, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { - return nil, err - } - if err := x.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type DRPCAnthropicService_MessagesClient interface { - drpc.Stream - Recv() (*JSONPayload, error) -} - -type drpcAnthropicService_MessagesClient struct { - drpc.Stream -} - -func (x *drpcAnthropicService_MessagesClient) GetStream() drpc.Stream { - return x.Stream -} - -func (x *drpcAnthropicService_MessagesClient) Recv() (*JSONPayload, error) { - m := new(JSONPayload) - if err := x.MsgRecv(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { - return nil, err - } - return m, nil -} - -func (x *drpcAnthropicService_MessagesClient) RecvMsg(m *JSONPayload) error { - return x.MsgRecv(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}) -} - -type DRPCAnthropicServiceServer interface { - Messages(*JSONPayload, DRPCAnthropicService_MessagesStream) error -} - -type DRPCAnthropicServiceUnimplementedServer struct{} - -func (s *DRPCAnthropicServiceUnimplementedServer) Messages(*JSONPayload, DRPCAnthropicService_MessagesStream) error { - return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) -} - -type DRPCAnthropicServiceDescription struct{} - -func (DRPCAnthropicServiceDescription) NumMethods() int { return 1 } - -func (DRPCAnthropicServiceDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { - switch n { - case 0: - return "/aibridged.AnthropicService/Messages", drpcEncoding_File_aibridged_proto_aibridged_proto{}, - func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { - return nil, srv.(DRPCAnthropicServiceServer). - Messages( - in1.(*JSONPayload), - &drpcAnthropicService_MessagesStream{in2.(drpc.Stream)}, - ) - }, DRPCAnthropicServiceServer.Messages, true - default: - return "", nil, nil, nil, false - } -} - -func DRPCRegisterAnthropicService(mux drpc.Mux, impl DRPCAnthropicServiceServer) error { - return mux.Register(impl, DRPCAnthropicServiceDescription{}) -} - -type DRPCAnthropicService_MessagesStream interface { - drpc.Stream - Send(*JSONPayload) error -} - -type drpcAnthropicService_MessagesStream struct { - drpc.Stream -} - -func (x *drpcAnthropicService_MessagesStream) Send(m *JSONPayload) error { - return x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}) -} diff --git a/cli/server.go b/cli/server.go index 4786961d9b9fb..25c5e914de679 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1594,11 +1594,13 @@ func newProvisionerDaemon( } func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string) (*aibridged.Server, error) { + httpAddr := "0.0.0.0:0" // TODO: configurable. + return aibridged.New(func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { // This debounces calls to listen every second. // TODO: is this true / necessary? return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx, name) - }, coderAPI.Logger.Named("aibridged").With(slog.F("name", name))) + }, httpAddr, coderAPI.Logger.Named("aibridged").With(slog.F("name", name))) } // nolint: revive diff --git a/coderd/aibridge.go b/coderd/aibridge.go index 79362232a6216..f2333e081977c 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -1,24 +1,17 @@ package coderd import ( + "fmt" "net/http" - "time" + "net/http/httputil" + "net/url" "cdr.dev/slog" - "github.com/coder/coder/v2/aibridged" "github.com/coder/coder/v2/coderd/util/slice" ) -func (api *API) bridgeOpenAIRequest(rw http.ResponseWriter, r *http.Request) { - api.bridgeAIRequest(rw, r, aibridged.AIProviderOpenAI) -} - -func (api *API) bridgeAnthropicRequest(rw http.ResponseWriter, r *http.Request) { - api.bridgeAIRequest(rw, r, aibridged.AIProviderAnthropic) -} - -func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request, client any) { +func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() if len(api.AIBridgeDaemons) == 0 { @@ -26,6 +19,8 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request, client return } + // Random loadbalancing. + // TODO: introduce better strategy. server, err := slice.PickRandom(api.AIBridgeDaemons) if err != nil { api.Logger.Error(ctx, "failed to pick random AI bridge server", slog.Error(err)) @@ -33,25 +28,12 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request, client return } - // TODO: use same context or new? - c, err := api.CreateInMemoryOpenAIBridgeClient(ctx, server) + u, err := url.Parse(fmt.Sprintf("http://%s", server.BridgeAddr())) // TODO: TLS. if err != nil { - api.Logger.Error(ctx, "failed to create OpenAI bridge", slog.Error(err)) - http.Error(rw, "failed to create OpenAI bridge", http.StatusInternalServerError) - return - } - - // TODO: don't create a new proxy on each request. - proxy, err := aibridged.NewDRPCProxy(aibridged.NewOpenAIAdapter(c), aibridged.ProxyConfig{ - ReadTimeout: time.Second * 60, // TODO: read timeout. - - }) - - if err != nil { - api.Logger.Error(ctx, "failed to proxy HTTP request to AI bridge daemon", slog.Error(err)) - http.Error(rw, "failed to proxy HTTP request to AI bridge", http.StatusInternalServerError) - return + api.Logger.Error(ctx, "failed to parse bridge address", slog.Error(err)) + http.Error(rw, "failed to parse bridge address", http.StatusInternalServerError) } - http.StripPrefix("/api/v2/aibridge", proxy).ServeHTTP(rw, r) + rp := httputil.NewSingleHostReverseProxy(u) + http.StripPrefix("/api/v2/aibridge", rp).ServeHTTP(rw, r) } diff --git a/coderd/coderd.go b/coderd/coderd.go index d317dc5563f55..7c56355694995 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1495,8 +1495,8 @@ func New(options *Options) *API { r.Get("/", api.tailnetRPCConn) }) r.Route("/aibridge", func(r chi.Router) { - r.Post("/v1/chat/completions", api.bridgeOpenAIRequest) - r.Post("/v1/messages", api.bridgeAnthropicRequest) + r.Post("/v1/chat/completions", api.bridgeAIRequest) + r.Post("/v1/messages", api.bridgeAIRequest) }) }) @@ -1930,59 +1930,59 @@ func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name strin return aibridgedproto.NewDRPCAIBridgeDaemonClient(clientSession), nil } -// TODO: naming... -func (api *API) CreateInMemoryOpenAIBridgeClient(dialCtx context.Context, srv *aibridged.Server) (client aibridgedproto.DRPCOpenAIServiceClient, err error) { - // TODO(dannyk): implement options. - // TODO(dannyk): implement tracing. - - clientSession, serverSession := drpcsdk.MemTransportPipe() - defer func() { - if err != nil { - _ = clientSession.Close() - _ = serverSession.Close() - } - }() - - // TODO(dannyk): implement API versioning. - - mux := drpcmux.New() - api.Logger.Debug(dialCtx, "starting in-memory OpenAI bridge") - logger := api.Logger.Named("inmem-openai-bridge") - err = aibridgedproto.DRPCRegisterOpenAIService(mux, srv) - if err != nil { - return nil, err - } - server := drpcserver.NewWithOptions(&tracing.DRPCHandler{Handler: mux}, - drpcserver.Options{ - Manager: drpcsdk.DefaultDRPCOptions(nil), - Log: func(err error) { - if xerrors.Is(err, io.EOF) { - return - } - logger.Debug(dialCtx, "drpc server error", slog.Error(err)) - }, - }, - ) - // in-mem pipes aren't technically "websockets" but they have the same properties as far as the - // API is concerned: they are long-lived connections that we need to close before completing - // shutdown of the API. - api.WebsocketWaitMutex.Lock() - api.WebsocketWaitGroup.Add(1) - api.WebsocketWaitMutex.Unlock() - go func() { - defer api.WebsocketWaitGroup.Done() - // Here we pass the background context, since we want the server to keep serving until the - // client hangs up. The aibridged is local, in-mem, so there isn't a danger of losing contact with it and - // having a dead connection we don't know the status of. - err := server.Serve(context.Background(), serverSession) - logger.Info(dialCtx, "OpenAI bridge disconnected", slog.Error(err)) - // close the sessions, so we don't leak goroutines serving them. - _ = clientSession.Close() - _ = serverSession.Close() - }() - - return aibridgedproto.NewDRPCOpenAIServiceClient(clientSession), nil -} +//// TODO: naming... +//func (api *API) CreateInMemoryOpenAIBridgeClient(dialCtx context.Context, srv *aibridged.Server) (client aibridgedproto.DRPCOpenAIServiceClient, err error) { +// // TODO(dannyk): implement options. +// // TODO(dannyk): implement tracing. +// +// clientSession, serverSession := drpcsdk.MemTransportPipe() +// defer func() { +// if err != nil { +// _ = clientSession.Close() +// _ = serverSession.Close() +// } +// }() +// +// // TODO(dannyk): implement API versioning. +// +// mux := drpcmux.New() +// api.Logger.Debug(dialCtx, "starting in-memory OpenAI bridge") +// logger := api.Logger.Named("inmem-openai-bridge") +// err = aibridgedproto.DRPCRegisterOpenAIService(mux, srv) +// if err != nil { +// return nil, err +// } +// server := drpcserver.NewWithOptions(&tracing.DRPCHandler{Handler: mux}, +// drpcserver.Options{ +// Manager: drpcsdk.DefaultDRPCOptions(nil), +// Log: func(err error) { +// if xerrors.Is(err, io.EOF) { +// return +// } +// logger.Debug(dialCtx, "drpc server error", slog.Error(err)) +// }, +// }, +// ) +// // in-mem pipes aren't technically "websockets" but they have the same properties as far as the +// // API is concerned: they are long-lived connections that we need to close before completing +// // shutdown of the API. +// api.WebsocketWaitMutex.Lock() +// api.WebsocketWaitGroup.Add(1) +// api.WebsocketWaitMutex.Unlock() +// go func() { +// defer api.WebsocketWaitGroup.Done() +// // Here we pass the background context, since we want the server to keep serving until the +// // client hangs up. The aibridged is local, in-mem, so there isn't a danger of losing contact with it and +// // having a dead connection we don't know the status of. +// err := server.Serve(context.Background(), serverSession) +// logger.Info(dialCtx, "OpenAI bridge disconnected", slog.Error(err)) +// // close the sessions, so we don't leak goroutines serving them. +// _ = clientSession.Close() +// _ = serverSession.Close() +// }() +// +// return aibridgedproto.NewDRPCOpenAIServiceClient(clientSession), nil +//} func (api *API) DERPMap() *tailcfg.DERPMap { fn := api.DERPMapper.Load() From 5ca93571e7477d5fa9d1b41c911bfa62dd1a644e Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 4 Jun 2025 11:40:06 +0200 Subject: [PATCH 03/61] make gen Signed-off-by: Danny Kopping --- cli/testdata/server-config.yaml.golden | 8 +++++++- coderd/apidoc/docs.go | 8 ++++++++ coderd/apidoc/swagger.json | 8 ++++++++ docs/reference/api/general.md | 3 +++ docs/reference/api/schemas.md | 14 ++++++++++++++ 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 7403819a2d10b..5f517be7cd6d4 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -521,7 +521,9 @@ client: supportLinks: [] # Configure AI providers. # (default: , type: struct[codersdk.AIConfig]) -ai: {} +ai: + bridgeconfig: + daemons: 3 # External Authentication providers. # (default: , type: struct[[]codersdk.ExternalAuthConfig]) externalAuthProviders: [] @@ -708,3 +710,7 @@ workspace_prebuilds: # limit; disabled when set to zero. # (default: 3, type: int) failure_hard_limit: 3 +ai_bridge: + # TODO. + # (default: 3, type: int) + daemons: 3 diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 16a51d187a486..f52339e5214be 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10856,6 +10856,14 @@ const docTemplate = `{ "codersdk.AIConfig": { "type": "object", "properties": { + "bridge": { + "type": "object", + "properties": { + "daemons": { + "type": "integer" + } + } + }, "providers": { "type": "array", "items": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 986862df59a09..5f57cfa7c7535 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9651,6 +9651,14 @@ "codersdk.AIConfig": { "type": "object", "properties": { + "bridge": { + "type": "object", + "properties": { + "daemons": { + "type": "integer" + } + } + }, "providers": { "type": "array", "items": { diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 12454145569bb..a37dfa306277d 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -163,6 +163,9 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "agent_stat_refresh_interval": 0, "ai": { "value": { + "bridge": { + "daemons": 0 + }, "providers": [ { "base_url": "string", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index f8e2152f629be..49b1b284fd1c0 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -583,6 +583,9 @@ ```json { + "bridge": { + "daemons": 0 + }, "providers": [ { "base_url": "string", @@ -599,6 +602,8 @@ | Name | Type | Required | Restrictions | Description | |-------------|-----------------------------------------------------------------|----------|--------------|-------------| +| `bridge` | object | false | | | +| `» daemons` | integer | false | | | | `providers` | array of [codersdk.AIProviderConfig](#codersdkaiproviderconfig) | false | | | ## codersdk.AIProviderConfig @@ -2334,6 +2339,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "agent_stat_refresh_interval": 0, "ai": { "value": { + "bridge": { + "daemons": 0 + }, "providers": [ { "base_url": "string", @@ -2833,6 +2841,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "agent_stat_refresh_interval": 0, "ai": { "value": { + "bridge": { + "daemons": 0 + }, "providers": [ { "base_url": "string", @@ -11792,6 +11803,9 @@ None ```json { "value": { + "bridge": { + "daemons": 0 + }, "providers": [ { "base_url": "string", From 667374f495b77184f6d1b920d0e1a0a8c5ad81ed Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 4 Jun 2025 14:41:02 +0200 Subject: [PATCH 04/61] proxying to providers Signed-off-by: Danny Kopping --- aibridged/bridge.go | 91 ++++++++++++++++++++++++------- aibridged/http_proxy.go | 118 ---------------------------------------- 2 files changed, 72 insertions(+), 137 deletions(-) delete mode 100644 aibridged/http_proxy.go diff --git a/aibridged/bridge.go b/aibridged/bridge.go index c95db9f654041..bf955d1250dbc 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -4,19 +4,23 @@ import ( "fmt" "net" "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" "golang.org/x/xerrors" ) type Bridge struct { - srv *http.Server - addr string + httpSrv *http.Server + addr string } func NewBridge(addr string) *Bridge { mux := &http.ServeMux{} - mux.HandleFunc("/v1/chat/completions", bridgeOpenAIRequest) - mux.HandleFunc("/v1/messages", bridgeAnthropicRequest) + mux.HandleFunc("/v1/chat/completions", proxyOpenAIRequest) + mux.HandleFunc("/v1/messages", proxyAnthropicRequest) srv := &http.Server{ Addr: addr, @@ -24,32 +28,81 @@ func NewBridge(addr string) *Bridge { // TODO: other settings. } - return &Bridge{srv: srv} + return &Bridge{httpSrv: srv} } -func (b *Bridge) Serve() error { - list, err := net.Listen("tcp", b.srv.Addr) +func proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { + target, err := url.Parse("https://api.openai.com") if err != nil { - return xerrors.Errorf("listen: %w", err) + http.Error(w, "failed to parse OpenAI URL", http.StatusInternalServerError) + return } - b.addr = list.Addr().String() + proxy := httputil.NewSingleHostReverseProxy(target) + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) - return b.srv.Serve(list) // TODO: TLS. -} + // Add OpenAI-specific headers + if strings.TrimSpace(req.Header.Get("Authorization")) == "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("OPENAI_API_KEY"))) + } -func (b *Bridge) Addr() string { - return b.addr -} + if req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/json") + } -func bridgeOpenAIRequest(w http.ResponseWriter, r *http.Request) { - fmt.Println("OpenAI") + req.Host = target.Host + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + + fmt.Printf("Proxying %s request to: %s\n", req.Method, req.URL.String()) + } + proxy.ServeHTTP(w, r) } -func bridgeAnthropicRequest(w http.ResponseWriter, r *http.Request) { - fmt.Println("Anthropic") +func proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { + target, err := url.Parse("https://api.anthropic.com") + if err != nil { + http.Error(w, "failed to parse Anthropic URL", http.StatusInternalServerError) + return + } + + proxy := httputil.NewSingleHostReverseProxy(target) + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) + + // Add Anthropic-specific headers + if strings.TrimSpace(req.Header.Get("x-api-key")) == "" { + req.Header.Set("x-api-key", os.Getenv("ANTHROPIC_API_KEY")) + } + req.Header.Set("anthropic-version", "2023-06-01") + + if req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/json") + } + + req.Host = target.Host + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + + fmt.Printf("Proxying %s request to: %s\n", req.Method, req.URL.String()) + } + proxy.ServeHTTP(w, r) } -func bridgeRequest(w http.ResponseWriter, r *http.Request) { +func (b *Bridge) Serve() error { + list, err := net.Listen("tcp", b.httpSrv.Addr) + if err != nil { + return xerrors.Errorf("listen: %w", err) + } + b.addr = list.Addr().String() + + return b.httpSrv.Serve(list) // TODO: TLS. +} + +func (b *Bridge) Addr() string { + return b.addr } diff --git a/aibridged/http_proxy.go b/aibridged/http_proxy.go deleted file mode 100644 index eb41821d06dc4..0000000000000 --- a/aibridged/http_proxy.go +++ /dev/null @@ -1,118 +0,0 @@ -package aibridged - -//import ( -// "context" -// "encoding/json" -// "fmt" -// "io" -// "net/http" -// "time" -// -// "cdr.dev/slog" -// "storj.io/drpc/drpcmux" -// -// "github.com/coder/coder/v2/aibridged/proto" -//) -// -//type AIProvider string -// -//const ( -// AIProviderOpenAI AIProvider = "openai" -// AIProviderAnthropic AIProvider = "anthropic" -//) -// -//type ProxyConfig struct { -// ReadTimeout time.Duration -//} -// -//type HTTPProxy struct { -// logger slog.Logger -// provider AIProvider -// config ProxyConfig -// mux *drpcmux.Mux -// drpcClient AIServiceClient -//} -// -//// NewDRPCProxy creates a new reverse proxy instance. -//func NewDRPCProxy(client AIServiceClient, config ProxyConfig) (*HTTPProxy, error) { -// return &HTTPProxy{ -// config: config, -// mux: drpcmux.New(), -// drpcClient: client, -// }, nil -//} // ServeHTTP handles incoming HTTP requests and proxies them to dRPC -// -//func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { -// // Read request body -// body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MiB // TODO: make configurable. -// if err != nil { -// http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest) -// return -// } -// defer r.Body.Close() -// -// // TODO: use body -// _ = body -// -// // Create context with timeout -// ctx, cancel := context.WithTimeout(r.Context(), p.config.ReadTimeout) -// defer cancel() -// -// switch p.drpcClient.(type) { -// case *OpenAIAdapter: -// payload := struct { -// messages []struct { -// Role string `json:"role,omitempty"` -// Content string `json:"content,omitempty"` -// } -// model string -// }{ -// messages: []struct { -// Role string `json:"role,omitempty"` -// Content string `json:"content,omitempty"` -// }{ -// { -// Role: "developer", -// Content: "what is 9/0?", -// }, -// }, -// model: "gpt-4o", -// } -// payloadJSON, err := json.Marshal(payload) -// if err != nil { -// // TODO: better error. -// http.Error(w, "failed to marshal payload", http.StatusInternalServerError) -// return -// } -// -// resp, err := p.drpcClient.SendRequest(ctx, &proto.JSONPayload{ -// Content: string(payloadJSON), -// }) -// if err != nil { -// return -// } -// -// // TODO: start SSE -// for { -// respPayload, err := resp.Recv() -// if err != nil { -// if err == io.EOF { -// return -// } -// http.Error(w, "failed to receive payload", http.StatusInternalServerError) -// return -// } -// -// _, _ = w.Write([]byte(respPayload.Content)) -// } -// -// // Set appropriate headers -// //w.Header().Set("Content-Type", "application/json") -// -// // Write response -// //if _, err := w.Write([]byte(resp.)); err != nil { -// // p.logger.Warn(ctx, "failed to write response: %w", err) -// //} -// case *AnthropicAdapter: -// } -//} From 91755c20ef7bd2bbc2e85bd71b94883f1bd3f762 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 4 Jun 2025 18:22:13 +0200 Subject: [PATCH 05/61] WIP Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 5 +- aibridged/bridge.go | 202 +++++++++++++++++- cli/server.go | 2 +- coderd/database/dbauthz/dbauthz.go | 4 + coderd/database/dbmem/dbmem.go | 9 + coderd/database/dbmetrics/querymetrics.go | 7 + coderd/database/dbmock/dbmock.go | 14 ++ coderd/database/dump.sql | 7 + .../migrations/000334_aibridge.up.sql | 7 + coderd/database/models.go | 7 + coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 15 ++ coderd/database/queries/wormhole.sql | 3 + go.mod | 7 + go.sum | 6 + 15 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 coderd/database/migrations/000334_aibridge.up.sql create mode 100644 coderd/database/queries/wormhole.sql diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 51a02321df24c..2939460945f25 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -15,6 +15,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/aibridged/proto" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/codersdk" ) @@ -51,13 +52,13 @@ type Server struct { bridge *Bridge } -func New(rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) { +func New(store database.Store, rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) { if rpcDialer == nil { return nil, xerrors.Errorf("nil rpcDialer given") } ctx, cancel := context.WithCancel(context.Background()) - bridge := NewBridge(httpAddr) + bridge := NewBridge(httpAddr, store) daemon := &Server{ logger: logger, clientDialer: rpcDialer, diff --git a/aibridged/bridge.go b/aibridged/bridge.go index bf955d1250dbc..b51b3d3096dd9 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -2,14 +2,25 @@ package aibridged import ( "fmt" + "log/slog" "net" "net/http" "net/http/httputil" "net/url" "os" "strings" + "time" + "github.com/charmbracelet/log" + "github.com/coder/freeway/apibridge" + "github.com/coder/freeway/middleware" + "github.com/coder/freeway/middleware/logger" + "github.com/coder/freeway/middleware/provider/anthropic" + "github.com/coder/freeway/middleware/provider/openai" + "github.com/coder/freeway/server" "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" ) type Bridge struct { @@ -17,10 +28,182 @@ type Bridge struct { addr string } -func NewBridge(addr string) *Bridge { +func NewBridge(addr string, store database.Store) *Bridge { + + // TODO: remove this. + { + handler := log.NewWithOptions(os.Stderr, log.Options{ + ReportCaller: true, // Enable caller reporting for debuggability + ReportTimestamp: true, + TimeFormat: time.TimeOnly, + }) + handler.SetLevel(log.DebugLevel) + slog.SetDefault(slog.New(handler)) + } + mux := &http.ServeMux{} mux.HandleFunc("/v1/chat/completions", proxyOpenAIRequest) - mux.HandleFunc("/v1/messages", proxyAnthropicRequest) + + openAIProvider, err := openai.NewOpenAIProvider("openai", openai.OpenAIProviderConfig{ + BaseURL: "https://api.openai.com/", + Model: "gpt-4o", + }) + if err != nil { + panic(err) // TODO: don't panic. + } + + anthropicProvider, err := anthropic.NewAnthropicProvider("anthropic", anthropic.AnthropicProviderConfig{ + Model: "claude-sonnet-4-0", + BaseURL: "https://api.anthropic.com/", + AnthropicVersion: "2023-06-01", + }) + if err != nil { + panic(err) // TODO: don't panic. + } + + rl, err := logger.NewRequestLogger(nil) + if err != nil { + panic(err) // TODO: don't panic. + } + + pipelines := map[string]server.ModelPipeline{ + "gpt-4o": { + Provider: openAIProvider, + }, + "claude-sonnet-4-0": { + Provider: anthropicProvider, + RequestChain: []middleware.RequestMiddleware{rl}, + //ResponseChain: []middleware.ResponseMiddleware{ + // func(req apibridge.Request, origResp apibridge.Response) (apibridge.Response, error) { + // baseID := origResp.ID + // modelName := req.Model + // messages := req.Messages + // + // //if resp.StopReason == "stop" { + // // event := map[string]any{ + // // "prompt": req.Messages, + // // "usage": resp.Usage, + // // } + // // eventBytes, err := json.Marshal(event) + // // if err != nil { + // // return resp, xerrors.Errorf("marshal event: %w", err) + // // } + // // + // // err = store.InsertWormholeEvent(context.TODO(), database.InsertWormholeEventParams{ + // // Event: eventBytes, + // // EventType: "anthropic", + // // }) + // // if err != nil { + // // return resp, xerrors.Errorf("wormhole: %w", err) + // // } + // //} + // // + // //return resp, nil + // + // resp := apibridge.Response{ + // ID: baseID, + // Model: modelName, + // StreamChannel: make(chan apibridge.StreamChunk), + // } + // + // go func() { + // defer close(resp.StreamChannel) + // + // var allParts []string + // for _, message := range messages { + // text := extractMessageText(message.Content) + // if text == "" { + // continue + // } + // rolePrefix := "unknown message: " + // switch message.Role { + // case apibridge.RoleSystem: + // rolePrefix = "system message: " + // case apibridge.RoleUser: + // rolePrefix = "user message: " + // case apibridge.RoleAssistant: // Should not happen in input, but handle + // rolePrefix = "assistant message: " + // } + // allParts = append(allParts, rolePrefix+text) + // } + // + // if len(allParts) == 0 { + // // If no content, send only a final chunk with finish reason + // finalChunk := apibridge.StreamChunk{ + // ID: resp.ID, + // Model: resp.Model, + // Choices: []apibridge.StreamChoice{ + // { + // Index: 0, + // Delta: apibridge.StreamChoiceDelta{}, // Empty delta + // FinishReason: "stop", + // }, + // }, + // Usage: &apibridge.Usage{}, // Empty usage + // } + // resp.StreamChannel <- finalChunk + // + // // Send the IsDone chunk for compatibility with server handlers + // // This is needed for the OpenAI handler to send the [DONE] marker + // doneChunk := apibridge.StreamChunk{ + // ID: resp.ID, + // Model: resp.Model, + // IsDone: true, + // } + // resp.StreamChannel <- doneChunk + // return + // } + // + // // Send each part as a separate chunk - consistent behavior + // for _, part := range allParts { + // contentChunk := apibridge.StreamChunk{ + // ID: resp.ID, + // Model: resp.Model, + // Choices: []apibridge.StreamChoice{ + // { + // Index: 0, + // Delta: apibridge.StreamChoiceDelta{ + // Role: apibridge.RoleAssistant, + // Content: []apibridge.ContentPart{{Type: apibridge.ContentTypeText, Text: part}}, + // }, + // }, + // }, + // } + // resp.StreamChannel <- contentChunk + // } + // + // // Send the final chunk with finish_reason + // finalChunk := apibridge.StreamChunk{ + // ID: resp.ID, + // Model: resp.Model, + // Choices: []apibridge.StreamChoice{ + // { + // Index: 0, + // Delta: apibridge.StreamChoiceDelta{}, // Empty delta for final chunk + // FinishReason: "stop", + // }, + // }, + // Usage: &apibridge.Usage{}, // Empty usage + // } + // resp.StreamChannel <- finalChunk + // + // // Send the IsDone chunk for compatibility with server handlers + // // This is needed for the OpenAI handler to send the [DONE] marker + // doneChunk := apibridge.StreamChunk{ + // ID: resp.ID, + // Model: resp.Model, + // IsDone: true, + // } + // resp.StreamChannel <- doneChunk + // }() + // + // return resp, nil + // }, + //}, + }, + } + + mux.HandleFunc("/v1/messages", server.RouteAnthropicMessages(pipelines)) srv := &http.Server{ Addr: addr, @@ -31,6 +214,16 @@ func NewBridge(addr string) *Bridge { return &Bridge{httpSrv: srv} } +func extractMessageText(content []apibridge.ContentPart) string { + var parts []string + for _, cp := range content { + if cp.Type == apibridge.ContentTypeText && cp.Text != "" { + parts = append(parts, cp.Text) + } + } + return strings.Join(parts, " ") +} + func proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { target, err := url.Parse("https://api.openai.com") if err != nil { @@ -89,6 +282,11 @@ func proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { fmt.Printf("Proxying %s request to: %s\n", req.Method, req.URL.String()) } + proxy.ModifyResponse = func(response *http.Response) error { + fmt.Println("response", response.ContentLength, response.Status) + + return nil + } proxy.ServeHTTP(w, r) } diff --git a/cli/server.go b/cli/server.go index 25c5e914de679..1e940899e5d5f 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1596,7 +1596,7 @@ func newProvisionerDaemon( func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string) (*aibridged.Server, error) { httpAddr := "0.0.0.0:0" // TODO: configurable. - return aibridged.New(func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { + return aibridged.New(coderAPI.Database, func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { // This debounces calls to listen every second. // TODO: is this true / necessary? return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx, name) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index c64701fde482a..45082212017cc 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3957,6 +3957,10 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab return q.db.InsertWorkspaceResourceMetadata(ctx, arg) } +func (q *querier) InsertWormholeEvent(ctx context.Context, arg database.InsertWormholeEventParams) error { + return q.db.InsertWormholeEvent(ctx, arg) // TODO: authz. +} + func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganization)(ctx, organizationID) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index f838a93d24c78..16ed5d90fd3b5 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -10178,6 +10178,15 @@ func (q *FakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg dat return metadata, nil } +func (q *FakeQuerier) InsertWormholeEvent(ctx context.Context, arg database.InsertWormholeEventParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + panic("not implemented") +} + func (q *FakeQuerier) ListProvisionerKeysByOrganization(_ context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index e208f9898cb1e..a964c16584fbf 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2482,6 +2482,13 @@ func (m queryMetricsStore) InsertWorkspaceResourceMetadata(ctx context.Context, return metadata, err } +func (m queryMetricsStore) InsertWormholeEvent(ctx context.Context, arg database.InsertWormholeEventParams) error { + start := time.Now() + r0 := m.s.InsertWormholeEvent(ctx, arg) + m.queryLatencies.WithLabelValues("InsertWormholeEvent").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { start := time.Now() r0, r1 := m.s.ListProvisionerKeysByOrganization(ctx, organizationID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index b6a04754f17b0..cc46fcada3518 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -5237,6 +5237,20 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(ctx, arg any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResourceMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResourceMetadata), ctx, arg) } +// InsertWormholeEvent mocks base method. +func (m *MockStore) InsertWormholeEvent(ctx context.Context, arg database.InsertWormholeEventParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertWormholeEvent", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertWormholeEvent indicates an expected call of InsertWormholeEvent. +func (mr *MockStoreMockRecorder) InsertWormholeEvent(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWormholeEvent", reflect.TypeOf((*MockStore)(nil).InsertWormholeEvent), ctx, arg) +} + // ListProvisionerKeysByOrganization mocks base method. func (m *MockStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 22a0b3d5a8adc..e9253dd26dbf7 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -2320,6 +2320,13 @@ CREATE VIEW workspaces_expanded AS COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; +CREATE TABLE wormhole ( + id uuid, + created_at timestamp with time zone NOT NULL, + event jsonb NOT NULL, + event_type character varying(32) NOT NULL +); + ALTER TABLE ONLY chat_messages ALTER COLUMN id SET DEFAULT nextval('chat_messages_id_seq'::regclass); ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass); diff --git a/coderd/database/migrations/000334_aibridge.up.sql b/coderd/database/migrations/000334_aibridge.up.sql new file mode 100644 index 0000000000000..0dfe95ef0d658 --- /dev/null +++ b/coderd/database/migrations/000334_aibridge.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE wormhole +( + id UUID, + created_at timestamptz NOT NULL, + event jsonb NOT NULL, + event_type varchar(32) NOT NULL +); diff --git a/coderd/database/models.go b/coderd/database/models.go index 69ae70b6c3bd3..7bcc649a37838 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3980,3 +3980,10 @@ type WorkspaceTable struct { Favorite bool `db:"favorite" json:"favorite"` NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` } + +type Wormhole struct { + ID uuid.NullUUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Event json.RawMessage `db:"event" json:"event"` + EventType string `db:"event_type" json:"event_type"` +} diff --git a/coderd/database/querier.go b/coderd/database/querier.go index b612143b63776..4b87771c81374 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -535,6 +535,7 @@ type sqlcQuerier interface { InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) + InsertWormholeEvent(ctx context.Context, arg InsertWormholeEventParams) error ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index eec91c7586d61..0c6f15cbaf686 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -19783,3 +19783,18 @@ func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg Insert } return items, nil } + +const insertWormholeEvent = `-- name: InsertWormholeEvent :exec +INSERT INTO wormhole (id, created_at, event, event_type) +VALUES (gen_random_uuid(), now(), $1, $2) +` + +type InsertWormholeEventParams struct { + Event json.RawMessage `db:"event" json:"event"` + EventType string `db:"event_type" json:"event_type"` +} + +func (q *sqlQuerier) InsertWormholeEvent(ctx context.Context, arg InsertWormholeEventParams) error { + _, err := q.db.ExecContext(ctx, insertWormholeEvent, arg.Event, arg.EventType) + return err +} diff --git a/coderd/database/queries/wormhole.sql b/coderd/database/queries/wormhole.sql new file mode 100644 index 0000000000000..3f21bcbdff8f2 --- /dev/null +++ b/coderd/database/queries/wormhole.sql @@ -0,0 +1,3 @@ +-- name: InsertWormholeEvent :exec +INSERT INTO wormhole (id, created_at, event, event_type) +VALUES (gen_random_uuid(), now(), @event, @event_type); diff --git a/go.mod b/go.mod index 1bc98d5f01b26..da5fab466887a 100644 --- a/go.mod +++ b/go.mod @@ -485,6 +485,8 @@ require ( require ( github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 + github.com/charmbracelet/log v0.4.1 + github.com/coder/freeway v0.0.0-20250514145842-67c0cca1dc72 github.com/coder/preview v0.0.2-0.20250527172548-ab173d35040c github.com/fsnotify/fsnotify v1.9.0 github.com/kylecarbs/aisdk-go v0.0.8 @@ -513,6 +515,8 @@ require ( github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/goccy/go-yaml v1.17.1 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/hashicorp/go-getter v1.7.8 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect @@ -531,3 +535,6 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect ) + +// TODO: remove +replace github.com/coder/freeway v0.0.0-20250514145842-67c0cca1dc72 => /home/coder/freeway diff --git a/go.sum b/go.sum index ff82f4db0ec17..b6f7be14b4213 100644 --- a/go.sum +++ b/go.sum @@ -849,6 +849,8 @@ github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk= +github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= @@ -1106,6 +1108,8 @@ github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6 github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -1156,6 +1160,8 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= +github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= From dcd3bed58afee18334df370af2f03bd8cb554c1d Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 6 Jun 2025 13:53:52 +0200 Subject: [PATCH 06/61] feat: track usage & prompts Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 34 +- aibridged/bridge.go | 321 +++-- aibridged/proto/aibridged.pb.go | 303 ++++- aibridged/proto/aibridged.proto | 14 + aibridged/proto/aibridged_drpc.pb.go | 82 +- cli/exp_mcp.go | 800 ------------- cli/server.go | 2 +- coderd/ai/ai.go | 167 --- coderd/aibridgedserver/aibridgedserver.go | 36 +- coderd/chat.go | 366 ------ coderd/chat_test.go | 125 -- coderd/coderd.go | 2 +- coderd/database/db2sdk/db2sdk.go | 3 +- codersdk/chat.go | 153 --- codersdk/toolsdk/toolsdk.go | 1333 --------------------- codersdk/toolsdk/toolsdk_test.go | 607 ---------- go.mod | 4 +- go.sum | 8 +- site/src/api/typesGenerated.ts | 2 + 19 files changed, 591 insertions(+), 3771 deletions(-) delete mode 100644 cli/exp_mcp.go delete mode 100644 coderd/ai/ai.go delete mode 100644 coderd/chat.go delete mode 100644 coderd/chat_test.go delete mode 100644 codersdk/chat.go delete mode 100644 codersdk/toolsdk/toolsdk.go delete mode 100644 codersdk/toolsdk/toolsdk_test.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 2939460945f25..04ec9d3d5457c 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -15,7 +15,6 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/aibridged/proto" - "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/codersdk" ) @@ -49,16 +48,17 @@ type Server struct { // shuttingDownCh will receive when we start graceful shutdown shuttingDownCh chan struct{} - bridge *Bridge + bridge *Bridge } -func New(store database.Store, rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) { +var _ proto.DRPCAIBridgeDaemonServer = &Server{} + +func New(rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) { if rpcDialer == nil { return nil, xerrors.Errorf("nil rpcDialer given") } ctx, cancel := context.WithCancel(context.Background()) - bridge := NewBridge(httpAddr, store) daemon := &Server{ logger: logger, clientDialer: rpcDialer, @@ -68,9 +68,11 @@ func New(store database.Store, rpcDialer Dialer, httpAddr string, logger slog.Lo closedCh: make(chan struct{}), shuttingDownCh: make(chan struct{}), initConnectionCh: make(chan struct{}), - - bridge: bridge, } + + bridge := NewBridge(httpAddr, daemon.client) + daemon.bridge = bridge + go daemon.connect() go func() { err := bridge.Serve() @@ -164,6 +166,26 @@ func (s *Server) AuditPrompt(ctx context.Context, in *proto.AuditPromptRequest) return out, nil } +func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { + out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackTokenUsageResponse, error) { + return client.TrackTokenUsage(ctx, in) + }) + if err != nil { + return nil, err + } + return out, nil +} + +func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPromptsRequest) (*proto.TrackUserPromptsResponse, error) { + out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackUserPromptsResponse, error) { + return client.TrackUserPrompts(ctx, in) + }) + if err != nil { + return nil, err + } + return out, nil +} + //func (s *Server) ChatCompletions(payload *proto.JSONPayload, stream proto.DRPCOpenAIService_ChatCompletionsStream) error { // // TODO: call OpenAI API. // diff --git a/aibridged/bridge.go b/aibridged/bridge.go index b51b3d3096dd9..fe316f8c9ef62 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -1,7 +1,11 @@ package aibridged import ( + "bytes" + "compress/gzip" + "encoding/json" "fmt" + "io" "log/slog" "net" "net/http" @@ -11,25 +15,22 @@ import ( "strings" "time" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/packages/ssestream" "github.com/charmbracelet/log" - "github.com/coder/freeway/apibridge" - "github.com/coder/freeway/middleware" - "github.com/coder/freeway/middleware/logger" - "github.com/coder/freeway/middleware/provider/anthropic" - "github.com/coder/freeway/middleware/provider/openai" - "github.com/coder/freeway/server" + "github.com/openai/openai-go" "golang.org/x/xerrors" - "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/aibridged/proto" ) type Bridge struct { - httpSrv *http.Server - addr string + httpSrv *http.Server + addr string + clientFn func() (proto.DRPCAIBridgeDaemonClient, bool) } -func NewBridge(addr string, store database.Store) *Bridge { - +func NewBridge(addr string, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) *Bridge { // TODO: remove this. { handler := log.NewWithOptions(os.Stderr, log.Options{ @@ -41,169 +42,11 @@ func NewBridge(addr string, store database.Store) *Bridge { slog.SetDefault(slog.New(handler)) } - mux := &http.ServeMux{} - mux.HandleFunc("/v1/chat/completions", proxyOpenAIRequest) - - openAIProvider, err := openai.NewOpenAIProvider("openai", openai.OpenAIProviderConfig{ - BaseURL: "https://api.openai.com/", - Model: "gpt-4o", - }) - if err != nil { - panic(err) // TODO: don't panic. - } - - anthropicProvider, err := anthropic.NewAnthropicProvider("anthropic", anthropic.AnthropicProviderConfig{ - Model: "claude-sonnet-4-0", - BaseURL: "https://api.anthropic.com/", - AnthropicVersion: "2023-06-01", - }) - if err != nil { - panic(err) // TODO: don't panic. - } - - rl, err := logger.NewRequestLogger(nil) - if err != nil { - panic(err) // TODO: don't panic. - } - - pipelines := map[string]server.ModelPipeline{ - "gpt-4o": { - Provider: openAIProvider, - }, - "claude-sonnet-4-0": { - Provider: anthropicProvider, - RequestChain: []middleware.RequestMiddleware{rl}, - //ResponseChain: []middleware.ResponseMiddleware{ - // func(req apibridge.Request, origResp apibridge.Response) (apibridge.Response, error) { - // baseID := origResp.ID - // modelName := req.Model - // messages := req.Messages - // - // //if resp.StopReason == "stop" { - // // event := map[string]any{ - // // "prompt": req.Messages, - // // "usage": resp.Usage, - // // } - // // eventBytes, err := json.Marshal(event) - // // if err != nil { - // // return resp, xerrors.Errorf("marshal event: %w", err) - // // } - // // - // // err = store.InsertWormholeEvent(context.TODO(), database.InsertWormholeEventParams{ - // // Event: eventBytes, - // // EventType: "anthropic", - // // }) - // // if err != nil { - // // return resp, xerrors.Errorf("wormhole: %w", err) - // // } - // //} - // // - // //return resp, nil - // - // resp := apibridge.Response{ - // ID: baseID, - // Model: modelName, - // StreamChannel: make(chan apibridge.StreamChunk), - // } - // - // go func() { - // defer close(resp.StreamChannel) - // - // var allParts []string - // for _, message := range messages { - // text := extractMessageText(message.Content) - // if text == "" { - // continue - // } - // rolePrefix := "unknown message: " - // switch message.Role { - // case apibridge.RoleSystem: - // rolePrefix = "system message: " - // case apibridge.RoleUser: - // rolePrefix = "user message: " - // case apibridge.RoleAssistant: // Should not happen in input, but handle - // rolePrefix = "assistant message: " - // } - // allParts = append(allParts, rolePrefix+text) - // } - // - // if len(allParts) == 0 { - // // If no content, send only a final chunk with finish reason - // finalChunk := apibridge.StreamChunk{ - // ID: resp.ID, - // Model: resp.Model, - // Choices: []apibridge.StreamChoice{ - // { - // Index: 0, - // Delta: apibridge.StreamChoiceDelta{}, // Empty delta - // FinishReason: "stop", - // }, - // }, - // Usage: &apibridge.Usage{}, // Empty usage - // } - // resp.StreamChannel <- finalChunk - // - // // Send the IsDone chunk for compatibility with server handlers - // // This is needed for the OpenAI handler to send the [DONE] marker - // doneChunk := apibridge.StreamChunk{ - // ID: resp.ID, - // Model: resp.Model, - // IsDone: true, - // } - // resp.StreamChannel <- doneChunk - // return - // } - // - // // Send each part as a separate chunk - consistent behavior - // for _, part := range allParts { - // contentChunk := apibridge.StreamChunk{ - // ID: resp.ID, - // Model: resp.Model, - // Choices: []apibridge.StreamChoice{ - // { - // Index: 0, - // Delta: apibridge.StreamChoiceDelta{ - // Role: apibridge.RoleAssistant, - // Content: []apibridge.ContentPart{{Type: apibridge.ContentTypeText, Text: part}}, - // }, - // }, - // }, - // } - // resp.StreamChannel <- contentChunk - // } - // - // // Send the final chunk with finish_reason - // finalChunk := apibridge.StreamChunk{ - // ID: resp.ID, - // Model: resp.Model, - // Choices: []apibridge.StreamChoice{ - // { - // Index: 0, - // Delta: apibridge.StreamChoiceDelta{}, // Empty delta for final chunk - // FinishReason: "stop", - // }, - // }, - // Usage: &apibridge.Usage{}, // Empty usage - // } - // resp.StreamChannel <- finalChunk - // - // // Send the IsDone chunk for compatibility with server handlers - // // This is needed for the OpenAI handler to send the [DONE] marker - // doneChunk := apibridge.StreamChunk{ - // ID: resp.ID, - // Model: resp.Model, - // IsDone: true, - // } - // resp.StreamChannel <- doneChunk - // }() - // - // return resp, nil - // }, - //}, - }, - } + var bridge Bridge - mux.HandleFunc("/v1/messages", server.RouteAnthropicMessages(pipelines)) + mux := &http.ServeMux{} + mux.HandleFunc("/v1/chat/completions", bridge.proxyOpenAIRequest) + mux.HandleFunc("/v1/messages", bridge.proxyAnthropicRequest) srv := &http.Server{ Addr: addr, @@ -211,20 +54,31 @@ func NewBridge(addr string, store database.Store) *Bridge { // TODO: other settings. } - return &Bridge{httpSrv: srv} + bridge.httpSrv = srv + bridge.clientFn = clientFn + + return &bridge } -func extractMessageText(content []apibridge.ContentPart) string { - var parts []string - for _, cp := range content { - if cp.Type == apibridge.ContentTypeText && cp.Text != "" { - parts = append(parts, cp.Text) - } +func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + // TODO: error handling. + panic(err) + return } - return strings.Join(parts, " ") -} + r.Body.Close() + + var msg openai.ChatCompletionNewParams + err = json.Unmarshal(body, &msg) + if err != nil { + // TODO: error handling. + panic(err) + return + } + + fmt.Println(msg) -func proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { target, err := url.Parse("https://api.openai.com") if err != nil { http.Error(w, "failed to parse OpenAI URL", http.StatusInternalServerError) @@ -254,7 +108,14 @@ func proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { proxy.ServeHTTP(w, r) } -func proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { +func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { + coderdClient, ok := b.clientFn() + if !ok { + // TODO: log issue. + http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) + return + } + target, err := url.Parse("https://api.anthropic.com") if err != nil { http.Error(w, "failed to parse Anthropic URL", http.StatusInternalServerError) @@ -280,10 +141,100 @@ func proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { req.URL.Scheme = target.Scheme req.URL.Host = target.Host + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, "could not ready request body", http.StatusBadRequest) + return + } + _ = req.Body.Close() + + var msg anthropic.MessageNewParams + err = json.NewDecoder(bytes.NewReader(body)).Decode(&msg) + if err != nil { + http.Error(w, "could not unmarshal request body", http.StatusBadRequest) + return + } + + // TODO: robustness + if len(msg.Messages) > 0 { + latest := msg.Messages[len(msg.Messages)-1] + if len(latest.Content) > 0 { + if latest.Content[0].OfText != nil { + _, _ = coderdClient.TrackUserPrompts(r.Context(), &proto.TrackUserPromptsRequest{ + Prompt: latest.Content[0].OfText.Text, + }) + } else { + fmt.Println() + } + } + } + + req.Body = io.NopCloser(bytes.NewReader(body)) + fmt.Printf("Proxying %s request to: %s\n", req.Method, req.URL.String()) } proxy.ModifyResponse = func(response *http.Response) error { - fmt.Println("response", response.ContentLength, response.Status) + body, err := io.ReadAll(response.Body) + if err != nil { + return xerrors.Errorf("read response body: %w", err) + } + if err = response.Body.Close(); err != nil { + return xerrors.Errorf("close body: %w", err) + } + + if !strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { + var msg anthropic.Message + + // TODO: check content-encoding to handle others. + gr, err := gzip.NewReader(bytes.NewReader(body)) + if err != nil { + return xerrors.Errorf("parse gzip-encoded body: %w", err) + } + + err = json.NewDecoder(gr).Decode(&msg) + if err != nil { + return xerrors.Errorf("parse non-streaming body: %w", err) + } + + _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ + MsgId: msg.ID, + InputTokens: msg.Usage.InputTokens, + OutputTokens: msg.Usage.OutputTokens, + }) + + response.Body = io.NopCloser(bytes.NewReader(body)) + return nil + } + + response.Body = io.NopCloser(bytes.NewReader(body)) + stream := ssestream.NewStream[anthropic.MessageStreamEventUnion](ssestream.NewDecoder(response), nil) + + var ( + inputToks, outputToks int64 + ) + + var msg anthropic.Message + for stream.Next() { + event := stream.Current() + err = msg.Accumulate(event) + if err != nil { + // TODO: don't panic. + panic(err) + } + + if msg.Usage.InputTokens+msg.Usage.OutputTokens > 0 { + inputToks = msg.Usage.InputTokens + outputToks = msg.Usage.OutputTokens + } + } + + _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ + MsgId: msg.ID, + InputTokens: inputToks, + OutputTokens: outputToks, + }) + + response.Body = io.NopCloser(bytes.NewReader(body)) return nil } diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index addcb6c0f7886..934ebc357941b 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -113,6 +113,192 @@ func (*AuditPromptResponse) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} } +type TrackTokenUsageRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MsgId string `protobuf:"bytes,1,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` + InputTokens int64 `protobuf:"varint,2,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` + OutputTokens int64 `protobuf:"varint,3,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` +} + +func (x *TrackTokenUsageRequest) Reset() { + *x = TrackTokenUsageRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TrackTokenUsageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrackTokenUsageRequest) ProtoMessage() {} + +func (x *TrackTokenUsageRequest) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrackTokenUsageRequest.ProtoReflect.Descriptor instead. +func (*TrackTokenUsageRequest) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} +} + +func (x *TrackTokenUsageRequest) GetMsgId() string { + if x != nil { + return x.MsgId + } + return "" +} + +func (x *TrackTokenUsageRequest) GetInputTokens() int64 { + if x != nil { + return x.InputTokens + } + return 0 +} + +func (x *TrackTokenUsageRequest) GetOutputTokens() int64 { + if x != nil { + return x.OutputTokens + } + return 0 +} + +type TrackTokenUsageResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TrackTokenUsageResponse) Reset() { + *x = TrackTokenUsageResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TrackTokenUsageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrackTokenUsageResponse) ProtoMessage() {} + +func (x *TrackTokenUsageResponse) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrackTokenUsageResponse.ProtoReflect.Descriptor instead. +func (*TrackTokenUsageResponse) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{3} +} + +type TrackUserPromptsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Prompt string `protobuf:"bytes,1,opt,name=prompt,proto3" json:"prompt,omitempty"` +} + +func (x *TrackUserPromptsRequest) Reset() { + *x = TrackUserPromptsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TrackUserPromptsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrackUserPromptsRequest) ProtoMessage() {} + +func (x *TrackUserPromptsRequest) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrackUserPromptsRequest.ProtoReflect.Descriptor instead. +func (*TrackUserPromptsRequest) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{4} +} + +func (x *TrackUserPromptsRequest) GetPrompt() string { + if x != nil { + return x.Prompt + } + return "" +} + +type TrackUserPromptsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TrackUserPromptsResponse) Reset() { + *x = TrackUserPromptsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TrackUserPromptsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrackUserPromptsResponse) ProtoMessage() {} + +func (x *TrackUserPromptsResponse) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrackUserPromptsResponse.ProtoReflect.Descriptor instead. +func (*TrackUserPromptsResponse) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} +} + var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ @@ -124,17 +310,42 @@ var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x13, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x5e, 0x0a, - 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, - 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1d, - 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, - 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, - 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, - 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, - 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x77, 0x0a, + 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, + 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x31, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, + 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, + 0x6f, 0x6d, 0x70, 0x74, 0x22, 0x1a, 0x0a, 0x18, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, + 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x32, 0x95, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, + 0x70, 0x74, 0x12, 0x1d, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, + 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, + 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, + 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, + 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x12, + 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -149,16 +360,24 @@ func file_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { return file_aibridged_proto_aibridged_proto_rawDescData } -var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ - (*AuditPromptRequest)(nil), // 0: aibridged.AuditPromptRequest - (*AuditPromptResponse)(nil), // 1: aibridged.AuditPromptResponse + (*AuditPromptRequest)(nil), // 0: aibridged.AuditPromptRequest + (*AuditPromptResponse)(nil), // 1: aibridged.AuditPromptResponse + (*TrackTokenUsageRequest)(nil), // 2: aibridged.TrackTokenUsageRequest + (*TrackTokenUsageResponse)(nil), // 3: aibridged.TrackTokenUsageResponse + (*TrackUserPromptsRequest)(nil), // 4: aibridged.TrackUserPromptsRequest + (*TrackUserPromptsResponse)(nil), // 5: aibridged.TrackUserPromptsResponse } var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ 0, // 0: aibridged.AIBridgeDaemon.AuditPrompt:input_type -> aibridged.AuditPromptRequest - 1, // 1: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type + 2, // 1: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest + 4, // 2: aibridged.AIBridgeDaemon.TrackUserPrompts:input_type -> aibridged.TrackUserPromptsRequest + 1, // 3: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse + 3, // 4: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse + 5, // 5: aibridged.AIBridgeDaemon.TrackUserPrompts:output_type -> aibridged.TrackUserPromptsResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -194,6 +413,54 @@ func file_aibridged_proto_aibridged_proto_init() { return nil } } + file_aibridged_proto_aibridged_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TrackTokenUsageRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_aibridged_proto_aibridged_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TrackTokenUsageResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_aibridged_proto_aibridged_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TrackUserPromptsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_aibridged_proto_aibridged_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TrackUserPromptsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -201,7 +468,7 @@ func file_aibridged_proto_aibridged_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_aibridged_proto_aibridged_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index b68f00fa108a1..1b6e045dc381e 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -10,10 +10,24 @@ message AuditPromptRequest { message AuditPromptResponse {} +message TrackTokenUsageRequest { + string msg_id = 1; + int64 input_tokens = 2; + int64 output_tokens = 3; +} +message TrackTokenUsageResponse {} + +message TrackUserPromptsRequest { + string prompt = 1; +} +message TrackUserPromptsResponse {} + // AIBridgeDaemon describes the service exposed by coderd, which allows the AI bridge to send data to coderd. service AIBridgeDaemon { // TODO: expand to full auditing; this is just a toy implementation for the moment. // AuditPrompt allows aibridged to report back to coderd that a prompt was issued by a user to an AI provider. rpc AuditPrompt(AuditPromptRequest) returns (AuditPromptResponse); + rpc TrackTokenUsage(TrackTokenUsageRequest) returns (TrackTokenUsageResponse); + rpc TrackUserPrompts(TrackUserPromptsRequest) returns (TrackUserPromptsResponse); } diff --git a/aibridged/proto/aibridged_drpc.pb.go b/aibridged/proto/aibridged_drpc.pb.go index d59200d81a885..d53aa983f2456 100644 --- a/aibridged/proto/aibridged_drpc.pb.go +++ b/aibridged/proto/aibridged_drpc.pb.go @@ -39,6 +39,8 @@ type DRPCAIBridgeDaemonClient interface { DRPCConn() drpc.Conn AuditPrompt(ctx context.Context, in *AuditPromptRequest) (*AuditPromptResponse, error) + TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) + TrackUserPrompts(ctx context.Context, in *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) } type drpcAIBridgeDaemonClient struct { @@ -60,8 +62,28 @@ func (c *drpcAIBridgeDaemonClient) AuditPrompt(ctx context.Context, in *AuditPro return out, nil } +func (c *drpcAIBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) { + out := new(TrackTokenUsageResponse) + err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackTokenUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *drpcAIBridgeDaemonClient) TrackUserPrompts(ctx context.Context, in *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) { + out := new(TrackUserPromptsResponse) + err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackUserPrompts", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + type DRPCAIBridgeDaemonServer interface { AuditPrompt(context.Context, *AuditPromptRequest) (*AuditPromptResponse, error) + TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) + TrackUserPrompts(context.Context, *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) } type DRPCAIBridgeDaemonUnimplementedServer struct{} @@ -70,9 +92,17 @@ func (s *DRPCAIBridgeDaemonUnimplementedServer) AuditPrompt(context.Context, *Au return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackUserPrompts(context.Context, *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + type DRPCAIBridgeDaemonDescription struct{} -func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 1 } +func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 3 } func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -85,6 +115,24 @@ func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc. in1.(*AuditPromptRequest), ) }, DRPCAIBridgeDaemonServer.AuditPrompt, true + case 1: + return "/aibridged.AIBridgeDaemon/TrackTokenUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAIBridgeDaemonServer). + TrackTokenUsage( + ctx, + in1.(*TrackTokenUsageRequest), + ) + }, DRPCAIBridgeDaemonServer.TrackTokenUsage, true + case 2: + return "/aibridged.AIBridgeDaemon/TrackUserPrompts", drpcEncoding_File_aibridged_proto_aibridged_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAIBridgeDaemonServer). + TrackUserPrompts( + ctx, + in1.(*TrackUserPromptsRequest), + ) + }, DRPCAIBridgeDaemonServer.TrackUserPrompts, true default: return "", nil, nil, nil, false } @@ -109,3 +157,35 @@ func (x *drpcAIBridgeDaemon_AuditPromptStream) SendAndClose(m *AuditPromptRespon } return x.CloseSend() } + +type DRPCAIBridgeDaemon_TrackTokenUsageStream interface { + drpc.Stream + SendAndClose(*TrackTokenUsageResponse) error +} + +type drpcAIBridgeDaemon_TrackTokenUsageStream struct { + drpc.Stream +} + +func (x *drpcAIBridgeDaemon_TrackTokenUsageStream) SendAndClose(m *TrackTokenUsageResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { + return err + } + return x.CloseSend() +} + +type DRPCAIBridgeDaemon_TrackUserPromptsStream interface { + drpc.Stream + SendAndClose(*TrackUserPromptsResponse) error +} + +type drpcAIBridgeDaemon_TrackUserPromptsStream struct { + drpc.Stream +} + +func (x *drpcAIBridgeDaemon_TrackUserPromptsStream) SendAndClose(m *TrackUserPromptsResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { + return err + } + return x.CloseSend() +} diff --git a/cli/exp_mcp.go b/cli/exp_mcp.go deleted file mode 100644 index 65f749c726963..0000000000000 --- a/cli/exp_mcp.go +++ /dev/null @@ -1,800 +0,0 @@ -package cli - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "net/url" - "os" - "path/filepath" - "slices" - "strings" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - "github.com/spf13/afero" - "golang.org/x/xerrors" - - "github.com/coder/coder/v2/buildinfo" - "github.com/coder/coder/v2/cli/cliui" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/coder/coder/v2/codersdk/toolsdk" - "github.com/coder/serpent" -) - -func (r *RootCmd) mcpCommand() *serpent.Command { - cmd := &serpent.Command{ - Use: "mcp", - Short: "Run the Coder MCP server and configure it to work with AI tools.", - Long: "The Coder MCP server allows you to automatically create workspaces with parameters.", - Handler: func(i *serpent.Invocation) error { - return i.Command.HelpHandler(i) - }, - Children: []*serpent.Command{ - r.mcpConfigure(), - r.mcpServer(), - }, - } - return cmd -} - -func (r *RootCmd) mcpConfigure() *serpent.Command { - cmd := &serpent.Command{ - Use: "configure", - Short: "Automatically configure the MCP server.", - Handler: func(i *serpent.Invocation) error { - return i.Command.HelpHandler(i) - }, - Children: []*serpent.Command{ - r.mcpConfigureClaudeDesktop(), - r.mcpConfigureClaudeCode(), - r.mcpConfigureCursor(), - }, - } - return cmd -} - -func (*RootCmd) mcpConfigureClaudeDesktop() *serpent.Command { - cmd := &serpent.Command{ - Use: "claude-desktop", - Short: "Configure the Claude Desktop server.", - Handler: func(_ *serpent.Invocation) error { - configPath, err := os.UserConfigDir() - if err != nil { - return err - } - configPath = filepath.Join(configPath, "Claude") - err = os.MkdirAll(configPath, 0o755) - if err != nil { - return err - } - configPath = filepath.Join(configPath, "claude_desktop_config.json") - _, err = os.Stat(configPath) - if err != nil { - if !os.IsNotExist(err) { - return err - } - } - contents := map[string]any{} - data, err := os.ReadFile(configPath) - if err != nil { - if !os.IsNotExist(err) { - return err - } - } else { - err = json.Unmarshal(data, &contents) - if err != nil { - return err - } - } - binPath, err := os.Executable() - if err != nil { - return err - } - contents["mcpServers"] = map[string]any{ - "coder": map[string]any{"command": binPath, "args": []string{"exp", "mcp", "server"}}, - } - data, err = json.MarshalIndent(contents, "", " ") - if err != nil { - return err - } - err = os.WriteFile(configPath, data, 0o600) - if err != nil { - return err - } - return nil - }, - } - return cmd -} - -func (*RootCmd) mcpConfigureClaudeCode() *serpent.Command { - var ( - claudeAPIKey string - claudeConfigPath string - claudeMDPath string - systemPrompt string - coderPrompt string - appStatusSlug string - testBinaryName string - - deprecatedCoderMCPClaudeAPIKey string - ) - cmd := &serpent.Command{ - Use: "claude-code ", - Short: "Configure the Claude Code server. You will need to run this command for each project you want to use. Specify the project directory as the first argument.", - Handler: func(inv *serpent.Invocation) error { - if len(inv.Args) == 0 { - return xerrors.Errorf("project directory is required") - } - projectDirectory := inv.Args[0] - fs := afero.NewOsFs() - binPath, err := os.Executable() - if err != nil { - return xerrors.Errorf("failed to get executable path: %w", err) - } - if testBinaryName != "" { - binPath = testBinaryName - } - configureClaudeEnv := map[string]string{} - agentToken, err := getAgentToken(fs) - if err != nil { - cliui.Warnf(inv.Stderr, "failed to get agent token: %s", err) - } else { - configureClaudeEnv["CODER_AGENT_TOKEN"] = agentToken - } - if claudeAPIKey == "" { - if deprecatedCoderMCPClaudeAPIKey == "" { - cliui.Warnf(inv.Stderr, "CLAUDE_API_KEY is not set.") - } else { - cliui.Warnf(inv.Stderr, "CODER_MCP_CLAUDE_API_KEY is deprecated, use CLAUDE_API_KEY instead") - claudeAPIKey = deprecatedCoderMCPClaudeAPIKey - } - } - if appStatusSlug != "" { - configureClaudeEnv["CODER_MCP_APP_STATUS_SLUG"] = appStatusSlug - } - if deprecatedSystemPromptEnv, ok := os.LookupEnv("SYSTEM_PROMPT"); ok { - cliui.Warnf(inv.Stderr, "SYSTEM_PROMPT is deprecated, use CODER_MCP_CLAUDE_SYSTEM_PROMPT instead") - systemPrompt = deprecatedSystemPromptEnv - } - - if err := configureClaude(fs, ClaudeConfig{ - // TODO: will this always be stable? - AllowedTools: []string{`mcp__coder__coder_report_task`}, - APIKey: claudeAPIKey, - ConfigPath: claudeConfigPath, - ProjectDirectory: projectDirectory, - MCPServers: map[string]ClaudeConfigMCP{ - "coder": { - Command: binPath, - Args: []string{"exp", "mcp", "server"}, - Env: configureClaudeEnv, - }, - }, - }); err != nil { - return xerrors.Errorf("failed to modify claude.json: %w", err) - } - cliui.Infof(inv.Stderr, "Wrote config to %s", claudeConfigPath) - - // Determine if we should include the reportTaskPrompt - var reportTaskPrompt string - if agentToken != "" && appStatusSlug != "" { - // Only include the report task prompt if both agent token and app - // status slug are defined. Otherwise, reporting a task will fail - // and confuse the agent (and by extension, the user). - reportTaskPrompt = defaultReportTaskPrompt - } - - // The Coder Prompt just allows users to extend our - if coderPrompt != "" { - reportTaskPrompt += "\n\n" + coderPrompt - } - - // We also write the system prompt to the CLAUDE.md file. - if err := injectClaudeMD(fs, reportTaskPrompt, systemPrompt, claudeMDPath); err != nil { - return xerrors.Errorf("failed to modify CLAUDE.md: %w", err) - } - cliui.Infof(inv.Stderr, "Wrote CLAUDE.md to %s", claudeMDPath) - return nil - }, - Options: []serpent.Option{ - { - Name: "claude-config-path", - Description: "The path to the Claude config file.", - Env: "CODER_MCP_CLAUDE_CONFIG_PATH", - Flag: "claude-config-path", - Value: serpent.StringOf(&claudeConfigPath), - Default: filepath.Join(os.Getenv("HOME"), ".claude.json"), - }, - { - Name: "claude-md-path", - Description: "The path to CLAUDE.md.", - Env: "CODER_MCP_CLAUDE_MD_PATH", - Flag: "claude-md-path", - Value: serpent.StringOf(&claudeMDPath), - Default: filepath.Join(os.Getenv("HOME"), ".claude", "CLAUDE.md"), - }, - { - Name: "claude-api-key", - Description: "The API key to use for the Claude Code server. This is also read from CLAUDE_API_KEY.", - Env: "CLAUDE_API_KEY", - Flag: "claude-api-key", - Value: serpent.StringOf(&claudeAPIKey), - }, - { - Name: "mcp-claude-api-key", - Description: "Hidden alias for CLAUDE_API_KEY. This will be removed in a future version.", - Env: "CODER_MCP_CLAUDE_API_KEY", - Value: serpent.StringOf(&deprecatedCoderMCPClaudeAPIKey), - Hidden: true, - }, - { - Name: "system-prompt", - Description: "The system prompt to use for the Claude Code server.", - Env: "CODER_MCP_CLAUDE_SYSTEM_PROMPT", - Flag: "claude-system-prompt", - Value: serpent.StringOf(&systemPrompt), - Default: "Send a task status update to notify the user that you are ready for input, and then wait for user input.", - }, - { - Name: "coder-prompt", - Description: "The coder prompt to use for the Claude Code server.", - Env: "CODER_MCP_CLAUDE_CODER_PROMPT", - Flag: "claude-coder-prompt", - Value: serpent.StringOf(&coderPrompt), - Default: "", // Empty default means we'll use defaultCoderPrompt from the variable - }, - { - Name: "app-status-slug", - Description: "The app status slug to use when running the Coder MCP server.", - Env: "CODER_MCP_APP_STATUS_SLUG", - Flag: "claude-app-status-slug", - Value: serpent.StringOf(&appStatusSlug), - }, - { - Name: "test-binary-name", - Description: "Only used for testing.", - Env: "CODER_MCP_CLAUDE_TEST_BINARY_NAME", - Flag: "claude-test-binary-name", - Value: serpent.StringOf(&testBinaryName), - Hidden: true, - }, - }, - } - return cmd -} - -func (*RootCmd) mcpConfigureCursor() *serpent.Command { - var project bool - cmd := &serpent.Command{ - Use: "cursor", - Short: "Configure Cursor to use Coder MCP.", - Options: serpent.OptionSet{ - serpent.Option{ - Flag: "project", - Env: "CODER_MCP_CURSOR_PROJECT", - Description: "Use to configure a local project to use the Cursor MCP.", - Value: serpent.BoolOf(&project), - }, - }, - Handler: func(_ *serpent.Invocation) error { - dir, err := os.Getwd() - if err != nil { - return err - } - if !project { - dir, err = os.UserHomeDir() - if err != nil { - return err - } - } - cursorDir := filepath.Join(dir, ".cursor") - err = os.MkdirAll(cursorDir, 0o755) - if err != nil { - return err - } - mcpConfig := filepath.Join(cursorDir, "mcp.json") - _, err = os.Stat(mcpConfig) - contents := map[string]any{} - if err != nil { - if !os.IsNotExist(err) { - return err - } - } else { - data, err := os.ReadFile(mcpConfig) - if err != nil { - return err - } - // The config can be empty, so we don't want to return an error if it is. - if len(data) > 0 { - err = json.Unmarshal(data, &contents) - if err != nil { - return err - } - } - } - mcpServers, ok := contents["mcpServers"].(map[string]any) - if !ok { - mcpServers = map[string]any{} - } - binPath, err := os.Executable() - if err != nil { - return err - } - mcpServers["coder"] = map[string]any{ - "command": binPath, - "args": []string{"exp", "mcp", "server"}, - } - contents["mcpServers"] = mcpServers - data, err := json.MarshalIndent(contents, "", " ") - if err != nil { - return err - } - err = os.WriteFile(mcpConfig, data, 0o600) - if err != nil { - return err - } - return nil - }, - } - return cmd -} - -func (r *RootCmd) mcpServer() *serpent.Command { - var ( - client = new(codersdk.Client) - instructions string - allowedTools []string - appStatusSlug string - ) - return &serpent.Command{ - Use: "server", - Handler: func(inv *serpent.Invocation) error { - return mcpServerHandler(inv, client, instructions, allowedTools, appStatusSlug) - }, - Short: "Start the Coder MCP server.", - Middleware: serpent.Chain( - r.TryInitClient(client), - ), - Options: []serpent.Option{ - { - Name: "instructions", - Description: "The instructions to pass to the MCP server.", - Flag: "instructions", - Env: "CODER_MCP_INSTRUCTIONS", - Value: serpent.StringOf(&instructions), - }, - { - Name: "allowed-tools", - Description: "Comma-separated list of allowed tools. If not specified, all tools are allowed.", - Flag: "allowed-tools", - Env: "CODER_MCP_ALLOWED_TOOLS", - Value: serpent.StringArrayOf(&allowedTools), - }, - { - Name: "app-status-slug", - Description: "When reporting a task, the coder_app slug under which to report the task.", - Flag: "app-status-slug", - Env: "CODER_MCP_APP_STATUS_SLUG", - Value: serpent.StringOf(&appStatusSlug), - Default: "", - }, - }, - } -} - -func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instructions string, allowedTools []string, appStatusSlug string) error { - ctx, cancel := context.WithCancel(inv.Context()) - defer cancel() - - fs := afero.NewOsFs() - - cliui.Infof(inv.Stderr, "Starting MCP server") - - // Check authentication status - var username string - - // Check authentication status first - if client != nil && client.URL != nil && client.SessionToken() != "" { - // Try to validate the client - me, err := client.User(ctx, codersdk.Me) - if err == nil { - username = me.Username - cliui.Infof(inv.Stderr, "Authentication : Successful") - cliui.Infof(inv.Stderr, "User : %s", username) - } else { - // Authentication failed but we have a client URL - cliui.Warnf(inv.Stderr, "Authentication : Failed (%s)", err) - cliui.Warnf(inv.Stderr, "Some tools that require authentication will not be available.") - } - } else { - cliui.Infof(inv.Stderr, "Authentication : None") - } - - // Display URL separately from authentication status - if client != nil && client.URL != nil { - cliui.Infof(inv.Stderr, "URL : %s", client.URL.String()) - } else { - cliui.Infof(inv.Stderr, "URL : Not configured") - } - - cliui.Infof(inv.Stderr, "Instructions : %q", instructions) - if len(allowedTools) > 0 { - cliui.Infof(inv.Stderr, "Allowed Tools : %v", allowedTools) - } - cliui.Infof(inv.Stderr, "Press Ctrl+C to stop the server") - - // Capture the original stdin, stdout, and stderr. - invStdin := inv.Stdin - invStdout := inv.Stdout - invStderr := inv.Stderr - defer func() { - inv.Stdin = invStdin - inv.Stdout = invStdout - inv.Stderr = invStderr - }() - - mcpSrv := server.NewMCPServer( - "Coder Agent", - buildinfo.Version(), - server.WithInstructions(instructions), - ) - - // Get the workspace agent token from the environment. - toolOpts := make([]func(*toolsdk.Deps), 0) - var hasAgentClient bool - - var agentURL *url.URL - if client != nil && client.URL != nil { - agentURL = client.URL - } else if agntURL, err := getAgentURL(); err == nil { - agentURL = agntURL - } - - // First check if we have a valid client URL, which is required for agent client - if agentURL == nil { - cliui.Infof(inv.Stderr, "Agent URL : Not configured") - } else { - cliui.Infof(inv.Stderr, "Agent URL : %s", agentURL.String()) - agentToken, err := getAgentToken(fs) - if err != nil || agentToken == "" { - cliui.Warnf(inv.Stderr, "CODER_AGENT_TOKEN is not set, task reporting will not be available") - } else { - // Happy path: we have both URL and agent token - agentClient := agentsdk.New(agentURL) - agentClient.SetSessionToken(agentToken) - toolOpts = append(toolOpts, toolsdk.WithAgentClient(agentClient)) - hasAgentClient = true - } - } - - if (client == nil || client.URL == nil || client.SessionToken() == "") && !hasAgentClient { - return xerrors.New(notLoggedInMessage) - } - - if appStatusSlug != "" { - toolOpts = append(toolOpts, toolsdk.WithAppStatusSlug(appStatusSlug)) - } else { - cliui.Warnf(inv.Stderr, "CODER_MCP_APP_STATUS_SLUG is not set, task reporting will not be available.") - } - - toolDeps, err := toolsdk.NewDeps(client, toolOpts...) - if err != nil { - return xerrors.Errorf("failed to initialize tool dependencies: %w", err) - } - - // Register tools based on the allowlist (if specified) - for _, tool := range toolsdk.All { - // Skip adding the coder_report_task tool if there is no agent client - if !hasAgentClient && tool.Tool.Name == "coder_report_task" { - cliui.Warnf(inv.Stderr, "Task reporting not available") - continue - } - - // Skip user-dependent tools if no authenticated user - if !tool.UserClientOptional && username == "" { - cliui.Warnf(inv.Stderr, "Tool %q requires authentication and will not be available", tool.Tool.Name) - continue - } - - if len(allowedTools) == 0 || slices.ContainsFunc(allowedTools, func(t string) bool { - return t == tool.Tool.Name - }) { - mcpSrv.AddTools(mcpFromSDK(tool, toolDeps)) - } - } - - srv := server.NewStdioServer(mcpSrv) - done := make(chan error) - go func() { - defer close(done) - srvErr := srv.Listen(ctx, invStdin, invStdout) - done <- srvErr - }() - - if err := <-done; err != nil { - if !errors.Is(err, context.Canceled) { - cliui.Errorf(inv.Stderr, "Failed to start the MCP server: %s", err) - return err - } - } - - return nil -} - -type ClaudeConfig struct { - ConfigPath string - ProjectDirectory string - APIKey string - AllowedTools []string - MCPServers map[string]ClaudeConfigMCP -} - -type ClaudeConfigMCP struct { - Command string `json:"command"` - Args []string `json:"args"` - Env map[string]string `json:"env"` -} - -func configureClaude(fs afero.Fs, cfg ClaudeConfig) error { - if cfg.ConfigPath == "" { - cfg.ConfigPath = filepath.Join(os.Getenv("HOME"), ".claude.json") - } - var config map[string]any - _, err := fs.Stat(cfg.ConfigPath) - if err != nil { - if !os.IsNotExist(err) { - return xerrors.Errorf("failed to stat claude config: %w", err) - } - // Touch the file to create it if it doesn't exist. - if err = afero.WriteFile(fs, cfg.ConfigPath, []byte(`{}`), 0o600); err != nil { - return xerrors.Errorf("failed to touch claude config: %w", err) - } - } - oldConfigBytes, err := afero.ReadFile(fs, cfg.ConfigPath) - if err != nil { - return xerrors.Errorf("failed to read claude config: %w", err) - } - err = json.Unmarshal(oldConfigBytes, &config) - if err != nil { - return xerrors.Errorf("failed to unmarshal claude config: %w", err) - } - - if cfg.APIKey != "" { - // Stops Claude from requiring the user to generate - // a Claude-specific API key. - config["primaryApiKey"] = cfg.APIKey - } - // Stops Claude from asking for onboarding. - config["hasCompletedOnboarding"] = true - // Stops Claude from asking for permissions. - config["bypassPermissionsModeAccepted"] = true - config["autoUpdaterStatus"] = "disabled" - // Stops Claude from asking for cost threshold. - config["hasAcknowledgedCostThreshold"] = true - - projects, ok := config["projects"].(map[string]any) - if !ok { - projects = make(map[string]any) - } - - project, ok := projects[cfg.ProjectDirectory].(map[string]any) - if !ok { - project = make(map[string]any) - } - - allowedTools, ok := project["allowedTools"].([]string) - if !ok { - allowedTools = []string{} - } - - // Add cfg.AllowedTools to the list if they're not already present. - for _, tool := range cfg.AllowedTools { - for _, existingTool := range allowedTools { - if tool == existingTool { - continue - } - } - allowedTools = append(allowedTools, tool) - } - project["allowedTools"] = allowedTools - project["hasTrustDialogAccepted"] = true - project["hasCompletedProjectOnboarding"] = true - - mcpServers, ok := project["mcpServers"].(map[string]any) - if !ok { - mcpServers = make(map[string]any) - } - for name, cfgmcp := range cfg.MCPServers { - mcpServers[name] = cfgmcp - } - project["mcpServers"] = mcpServers - // Prevents Claude from asking the user to complete the project onboarding. - project["hasCompletedProjectOnboarding"] = true - - history, ok := project["history"].([]string) - injectedHistoryLine := "make sure to read claude.md and report tasks properly" - - if !ok || len(history) == 0 { - // History doesn't exist or is empty, create it with our injected line - history = []string{injectedHistoryLine} - } else if history[0] != injectedHistoryLine { - // Check if our line is already the first item - // Prepend our line to the existing history - history = append([]string{injectedHistoryLine}, history...) - } - project["history"] = history - - projects[cfg.ProjectDirectory] = project - config["projects"] = projects - - newConfigBytes, err := json.MarshalIndent(config, "", " ") - if err != nil { - return xerrors.Errorf("failed to marshal claude config: %w", err) - } - err = afero.WriteFile(fs, cfg.ConfigPath, newConfigBytes, 0o644) - if err != nil { - return xerrors.Errorf("failed to write claude config: %w", err) - } - return nil -} - -var ( - defaultReportTaskPrompt = `Respect the requirements of the "coder_report_task" tool. It is pertinent to provide a fantastic user-experience.` - - // Define the guard strings - coderPromptStartGuard = "" - coderPromptEndGuard = "" - systemPromptStartGuard = "" - systemPromptEndGuard = "" -) - -func injectClaudeMD(fs afero.Fs, coderPrompt, systemPrompt, claudeMDPath string) error { - _, err := fs.Stat(claudeMDPath) - if err != nil { - if !os.IsNotExist(err) { - return xerrors.Errorf("failed to stat claude config: %w", err) - } - // Write a new file with the system prompt. - if err = fs.MkdirAll(filepath.Dir(claudeMDPath), 0o700); err != nil { - return xerrors.Errorf("failed to create claude config directory: %w", err) - } - - return afero.WriteFile(fs, claudeMDPath, []byte(promptsBlock(coderPrompt, systemPrompt, "")), 0o600) - } - - bs, err := afero.ReadFile(fs, claudeMDPath) - if err != nil { - return xerrors.Errorf("failed to read claude config: %w", err) - } - - // Extract the content without the guarded sections - cleanContent := string(bs) - - // Remove existing coder prompt section if it exists - coderStartIdx := indexOf(cleanContent, coderPromptStartGuard) - coderEndIdx := indexOf(cleanContent, coderPromptEndGuard) - if coderStartIdx != -1 && coderEndIdx != -1 && coderStartIdx < coderEndIdx { - beforeCoderPrompt := cleanContent[:coderStartIdx] - afterCoderPrompt := cleanContent[coderEndIdx+len(coderPromptEndGuard):] - cleanContent = beforeCoderPrompt + afterCoderPrompt - } - - // Remove existing system prompt section if it exists - systemStartIdx := indexOf(cleanContent, systemPromptStartGuard) - systemEndIdx := indexOf(cleanContent, systemPromptEndGuard) - if systemStartIdx != -1 && systemEndIdx != -1 && systemStartIdx < systemEndIdx { - beforeSystemPrompt := cleanContent[:systemStartIdx] - afterSystemPrompt := cleanContent[systemEndIdx+len(systemPromptEndGuard):] - cleanContent = beforeSystemPrompt + afterSystemPrompt - } - - // Trim any leading whitespace from the clean content - cleanContent = strings.TrimSpace(cleanContent) - - // Create the new content with coder and system prompt prepended - newContent := promptsBlock(coderPrompt, systemPrompt, cleanContent) - - // Write the updated content back to the file - err = afero.WriteFile(fs, claudeMDPath, []byte(newContent), 0o600) - if err != nil { - return xerrors.Errorf("failed to write claude config: %w", err) - } - - return nil -} - -func promptsBlock(coderPrompt, systemPrompt, existingContent string) string { - var newContent strings.Builder - _, _ = newContent.WriteString(coderPromptStartGuard) - _, _ = newContent.WriteRune('\n') - _, _ = newContent.WriteString(coderPrompt) - _, _ = newContent.WriteRune('\n') - _, _ = newContent.WriteString(coderPromptEndGuard) - _, _ = newContent.WriteRune('\n') - _, _ = newContent.WriteString(systemPromptStartGuard) - _, _ = newContent.WriteRune('\n') - _, _ = newContent.WriteString(systemPrompt) - _, _ = newContent.WriteRune('\n') - _, _ = newContent.WriteString(systemPromptEndGuard) - _, _ = newContent.WriteRune('\n') - if existingContent != "" { - _, _ = newContent.WriteString(existingContent) - } - return newContent.String() -} - -// indexOf returns the index of the first instance of substr in s, -// or -1 if substr is not present in s. -func indexOf(s, substr string) int { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return i - } - } - return -1 -} - -func getAgentToken(fs afero.Fs) (string, error) { - token, ok := os.LookupEnv("CODER_AGENT_TOKEN") - if ok && token != "" { - return token, nil - } - tokenFile, ok := os.LookupEnv("CODER_AGENT_TOKEN_FILE") - if !ok { - return "", xerrors.Errorf("CODER_AGENT_TOKEN or CODER_AGENT_TOKEN_FILE must be set for token auth") - } - bs, err := afero.ReadFile(fs, tokenFile) - if err != nil { - return "", xerrors.Errorf("failed to read agent token file: %w", err) - } - return string(bs), nil -} - -func getAgentURL() (*url.URL, error) { - urlString, ok := os.LookupEnv("CODER_AGENT_URL") - if !ok || urlString == "" { - return nil, xerrors.New("CODEDR_AGENT_URL is empty") - } - - return url.Parse(urlString) -} - -// mcpFromSDK adapts a toolsdk.Tool to go-mcp's server.ServerTool. -// It assumes that the tool responds with a valid JSON object. -func mcpFromSDK(sdkTool toolsdk.GenericTool, tb toolsdk.Deps) server.ServerTool { - // NOTE: some clients will silently refuse to use tools if there is an issue - // with the tool's schema or configuration. - if sdkTool.Schema.Properties == nil { - panic("developer error: schema properties cannot be nil") - } - return server.ServerTool{ - Tool: mcp.Tool{ - Name: sdkTool.Tool.Name, - Description: sdkTool.Description, - InputSchema: mcp.ToolInputSchema{ - Type: "object", // Default of mcp.NewTool() - Properties: sdkTool.Schema.Properties, - Required: sdkTool.Schema.Required, - }, - }, - Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(request.Params.Arguments); err != nil { - return nil, xerrors.Errorf("failed to encode request arguments: %w", err) - } - result, err := sdkTool.Handler(ctx, tb, buf.Bytes()) - if err != nil { - return nil, err - } - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(string(result)), - }, - }, nil - }, - } -} diff --git a/cli/server.go b/cli/server.go index 1e940899e5d5f..25c5e914de679 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1596,7 +1596,7 @@ func newProvisionerDaemon( func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string) (*aibridged.Server, error) { httpAddr := "0.0.0.0:0" // TODO: configurable. - return aibridged.New(coderAPI.Database, func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { + return aibridged.New(func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { // This debounces calls to listen every second. // TODO: is this true / necessary? return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx, name) diff --git a/coderd/ai/ai.go b/coderd/ai/ai.go deleted file mode 100644 index 97c825ae44c06..0000000000000 --- a/coderd/ai/ai.go +++ /dev/null @@ -1,167 +0,0 @@ -package ai - -import ( - "context" - - "github.com/anthropics/anthropic-sdk-go" - anthropicoption "github.com/anthropics/anthropic-sdk-go/option" - "github.com/kylecarbs/aisdk-go" - "github.com/openai/openai-go" - openaioption "github.com/openai/openai-go/option" - "golang.org/x/xerrors" - "google.golang.org/genai" - - "github.com/coder/coder/v2/codersdk" -) - -type LanguageModel struct { - codersdk.LanguageModel - StreamFunc StreamFunc -} - -type StreamOptions struct { - SystemPrompt string - Model string - Messages []aisdk.Message - Thinking bool - Tools []aisdk.Tool -} - -type StreamFunc func(ctx context.Context, options StreamOptions) (aisdk.DataStream, error) - -// LanguageModels is a map of language model ID to language model. -type LanguageModels map[string]LanguageModel - -func ModelsFromConfig(ctx context.Context, configs []codersdk.AIProviderConfig) (LanguageModels, error) { - models := make(LanguageModels) - - for _, config := range configs { - var streamFunc StreamFunc - - switch config.Type { - case "openai": - opts := []openaioption.RequestOption{ - openaioption.WithAPIKey(config.APIKey), - } - if config.BaseURL != "" { - opts = append(opts, openaioption.WithBaseURL(config.BaseURL)) - } - client := openai.NewClient(opts...) - streamFunc = func(ctx context.Context, options StreamOptions) (aisdk.DataStream, error) { - openaiMessages, err := aisdk.MessagesToOpenAI(options.Messages) - if err != nil { - return nil, err - } - tools := aisdk.ToolsToOpenAI(options.Tools) - if options.SystemPrompt != "" { - openaiMessages = append([]openai.ChatCompletionMessageParamUnion{ - openai.SystemMessage(options.SystemPrompt), - }, openaiMessages...) - } - - return aisdk.OpenAIToDataStream(client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{ - Messages: openaiMessages, - Model: options.Model, - Tools: tools, - MaxTokens: openai.Int(8192), - })), nil - } - if config.Models == nil { - models, err := client.Models.List(ctx) - if err != nil { - return nil, err - } - config.Models = make([]string, len(models.Data)) - for i, model := range models.Data { - config.Models[i] = model.ID - } - } - case "anthropic": - client := anthropic.NewClient(anthropicoption.WithAPIKey(config.APIKey)) - streamFunc = func(ctx context.Context, options StreamOptions) (aisdk.DataStream, error) { - anthropicMessages, systemMessage, err := aisdk.MessagesToAnthropic(options.Messages) - if err != nil { - return nil, err - } - if options.SystemPrompt != "" { - systemMessage = []anthropic.TextBlockParam{ - *anthropic.NewTextBlock(options.SystemPrompt).OfRequestTextBlock, - } - } - return aisdk.AnthropicToDataStream(client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ - Messages: anthropicMessages, - Model: options.Model, - System: systemMessage, - Tools: aisdk.ToolsToAnthropic(options.Tools), - MaxTokens: 8192, - })), nil - } - if config.Models == nil { - models, err := client.Models.List(ctx, anthropic.ModelListParams{}) - if err != nil { - return nil, err - } - config.Models = make([]string, len(models.Data)) - for i, model := range models.Data { - config.Models[i] = model.ID - } - } - case "google": - client, err := genai.NewClient(ctx, &genai.ClientConfig{ - APIKey: config.APIKey, - Backend: genai.BackendGeminiAPI, - }) - if err != nil { - return nil, err - } - streamFunc = func(ctx context.Context, options StreamOptions) (aisdk.DataStream, error) { - googleMessages, err := aisdk.MessagesToGoogle(options.Messages) - if err != nil { - return nil, err - } - tools, err := aisdk.ToolsToGoogle(options.Tools) - if err != nil { - return nil, err - } - var systemInstruction *genai.Content - if options.SystemPrompt != "" { - systemInstruction = &genai.Content{ - Parts: []*genai.Part{ - genai.NewPartFromText(options.SystemPrompt), - }, - Role: "model", - } - } - return aisdk.GoogleToDataStream(client.Models.GenerateContentStream(ctx, options.Model, googleMessages, &genai.GenerateContentConfig{ - SystemInstruction: systemInstruction, - Tools: tools, - })), nil - } - if config.Models == nil { - models, err := client.Models.List(ctx, &genai.ListModelsConfig{}) - if err != nil { - return nil, err - } - config.Models = make([]string, len(models.Items)) - for i, model := range models.Items { - config.Models[i] = model.Name - } - } - default: - return nil, xerrors.Errorf("unsupported model type: %s", config.Type) - } - - for _, model := range config.Models { - models[model] = LanguageModel{ - LanguageModel: codersdk.LanguageModel{ - ID: model, - DisplayName: model, - Provider: config.Type, - }, - StreamFunc: streamFunc, - } - } - } - - return models, nil -} diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index a9796f7532eff..5de272f5151d7 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -2,8 +2,12 @@ package aibridgedserver import ( "context" + "encoding/json" + + "golang.org/x/xerrors" "github.com/coder/coder/v2/aibridged/proto" + "github.com/coder/coder/v2/coderd/database" ) type Server struct { @@ -11,14 +15,44 @@ type Server struct { // as when the API server shuts down, we want to cancel any // long-running operations. lifecycleCtx context.Context + store database.Store } func (s *Server) AuditPrompt(_ context.Context, req *proto.AuditPromptRequest) (*proto.AuditPromptResponse, error) { return &proto.AuditPromptResponse{}, nil } -func NewServer(lifecycleCtx context.Context) (*Server, error) { +func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { + raw, err := json.Marshal(in) + if err != nil { + return nil, xerrors.Errorf("marshal event: %w", err) + } + + err = s.store.InsertWormholeEvent(ctx, database.InsertWormholeEventParams{Event: raw, EventType: "token_usage"}) + if err != nil { + return nil, xerrors.Errorf("store event: %w", err) + } + + return &proto.TrackTokenUsageResponse{}, nil +} + +func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPromptsRequest) (*proto.TrackUserPromptsResponse, error) { + raw, err := json.Marshal(in) + if err != nil { + return nil, xerrors.Errorf("marshal event: %w", err) + } + + err = s.store.InsertWormholeEvent(ctx, database.InsertWormholeEventParams{Event: raw, EventType: "user_prompt"}) + if err != nil { + return nil, xerrors.Errorf("store event: %w", err) + } + + return &proto.TrackUserPromptsResponse{}, nil +} + +func NewServer(lifecycleCtx context.Context, store database.Store) (*Server, error) { return &Server{ lifecycleCtx: lifecycleCtx, + store: store, }, nil } diff --git a/coderd/chat.go b/coderd/chat.go deleted file mode 100644 index b10211075cfe6..0000000000000 --- a/coderd/chat.go +++ /dev/null @@ -1,366 +0,0 @@ -package coderd - -import ( - "encoding/json" - "io" - "net/http" - "time" - - "github.com/kylecarbs/aisdk-go" - - "github.com/coder/coder/v2/coderd/ai" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" - "github.com/coder/coder/v2/coderd/database/dbtime" - "github.com/coder/coder/v2/coderd/httpapi" - "github.com/coder/coder/v2/coderd/httpmw" - "github.com/coder/coder/v2/coderd/util/strings" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/toolsdk" -) - -// postChats creates a new chat. -// -// @Summary Create a chat -// @ID create-a-chat -// @Security CoderSessionToken -// @Produce json -// @Tags Chat -// @Success 201 {object} codersdk.Chat -// @Router /chats [post] -func (api *API) postChats(w http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - ctx := r.Context() - - chat, err := api.Database.InsertChat(ctx, database.InsertChatParams{ - OwnerID: apiKey.UserID, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Title: "New Chat", - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to create chat", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, w, http.StatusCreated, db2sdk.Chat(chat)) -} - -// listChats lists all chats for a user. -// -// @Summary List chats -// @ID list-chats -// @Security CoderSessionToken -// @Produce json -// @Tags Chat -// @Success 200 {array} codersdk.Chat -// @Router /chats [get] -func (api *API) listChats(w http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - ctx := r.Context() - - chats, err := api.Database.GetChatsByOwnerID(ctx, apiKey.UserID) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to list chats", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, w, http.StatusOK, db2sdk.Chats(chats)) -} - -// chat returns a chat by ID. -// -// @Summary Get a chat -// @ID get-a-chat -// @Security CoderSessionToken -// @Produce json -// @Tags Chat -// @Param chat path string true "Chat ID" -// @Success 200 {object} codersdk.Chat -// @Router /chats/{chat} [get] -func (*API) chat(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - chat := httpmw.ChatParam(r) - httpapi.Write(ctx, w, http.StatusOK, db2sdk.Chat(chat)) -} - -// chatMessages returns the messages of a chat. -// -// @Summary Get chat messages -// @ID get-chat-messages -// @Security CoderSessionToken -// @Produce json -// @Tags Chat -// @Param chat path string true "Chat ID" -// @Success 200 {array} aisdk.Message -// @Router /chats/{chat}/messages [get] -func (api *API) chatMessages(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - chat := httpmw.ChatParam(r) - rawMessages, err := api.Database.GetChatMessagesByChatID(ctx, chat.ID) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to get chat messages", - Detail: err.Error(), - }) - return - } - messages := make([]aisdk.Message, len(rawMessages)) - for i, message := range rawMessages { - var msg aisdk.Message - err = json.Unmarshal(message.Content, &msg) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to unmarshal chat message", - Detail: err.Error(), - }) - return - } - messages[i] = msg - } - - httpapi.Write(ctx, w, http.StatusOK, messages) -} - -// postChatMessages creates a new chat message and streams the response. -// -// @Summary Create a chat message -// @ID create-a-chat-message -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Chat -// @Param chat path string true "Chat ID" -// @Param request body codersdk.CreateChatMessageRequest true "Request body" -// @Success 200 {array} aisdk.DataStreamPart -// @Router /chats/{chat}/messages [post] -func (api *API) postChatMessages(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - chat := httpmw.ChatParam(r) - var req codersdk.CreateChatMessageRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ - Message: "Failed to decode chat message", - Detail: err.Error(), - }) - return - } - - dbMessages, err := api.Database.GetChatMessagesByChatID(ctx, chat.ID) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to get chat messages", - Detail: err.Error(), - }) - return - } - - messages := make([]codersdk.ChatMessage, 0) - for _, dbMsg := range dbMessages { - var msg codersdk.ChatMessage - err = json.Unmarshal(dbMsg.Content, &msg) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to unmarshal chat message", - Detail: err.Error(), - }) - return - } - messages = append(messages, msg) - } - messages = append(messages, req.Message) - - client := codersdk.New(api.AccessURL) - client.SetSessionToken(httpmw.APITokenFromRequest(r)) - - tools := make([]aisdk.Tool, 0) - handlers := map[string]toolsdk.GenericHandlerFunc{} - for _, tool := range toolsdk.All { - if tool.Name == "coder_report_task" { - continue // This tool requires an agent to run. - } - tools = append(tools, tool.Tool) - handlers[tool.Tool.Name] = tool.Handler - } - - provider, ok := api.LanguageModels[req.Model] - if !ok { - httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ - Message: "Model not found", - }) - return - } - - // If it's the user's first message, generate a title for the chat. - if len(messages) == 1 { - var acc aisdk.DataStreamAccumulator - stream, err := provider.StreamFunc(ctx, ai.StreamOptions{ - Model: req.Model, - SystemPrompt: `- You will generate a short title based on the user's message. -- It should be maximum of 40 characters. -- Do not use quotes, colons, special characters, or emojis.`, - Messages: messages, - Tools: []aisdk.Tool{}, // This initial stream doesn't use tools. - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to create stream", - Detail: err.Error(), - }) - return - } - stream = stream.WithAccumulator(&acc) - err = stream.Pipe(io.Discard) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to pipe stream", - Detail: err.Error(), - }) - return - } - var newTitle string - accMessages := acc.Messages() - // If for some reason the stream didn't return any messages, use the - // original message as the title. - if len(accMessages) == 0 { - newTitle = strings.Truncate(messages[0].Content, 40) - } else { - newTitle = strings.Truncate(accMessages[0].Content, 40) - } - err = api.Database.UpdateChatByID(ctx, database.UpdateChatByIDParams{ - ID: chat.ID, - Title: newTitle, - UpdatedAt: dbtime.Now(), - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to update chat title", - Detail: err.Error(), - }) - return - } - } - - // Write headers for the data stream! - aisdk.WriteDataStreamHeaders(w) - - // Insert the user-requested message into the database! - raw, err := json.Marshal([]aisdk.Message{req.Message}) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to marshal chat message", - Detail: err.Error(), - }) - return - } - _, err = api.Database.InsertChatMessages(ctx, database.InsertChatMessagesParams{ - ChatID: chat.ID, - CreatedAt: dbtime.Now(), - Model: req.Model, - Provider: provider.Provider, - Content: raw, - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to insert chat messages", - Detail: err.Error(), - }) - return - } - - deps, err := toolsdk.NewDeps(client) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to create tool dependencies", - Detail: err.Error(), - }) - return - } - - for { - var acc aisdk.DataStreamAccumulator - stream, err := provider.StreamFunc(ctx, ai.StreamOptions{ - Model: req.Model, - Messages: messages, - Tools: tools, - SystemPrompt: `You are a chat assistant for Coder - an open-source platform for creating and managing cloud development environments on any infrastructure. You are expected to be precise, concise, and helpful. - -You are running as an agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. Do NOT guess or make up an answer.`, - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to create stream", - Detail: err.Error(), - }) - return - } - stream = stream.WithToolCalling(func(toolCall aisdk.ToolCall) aisdk.ToolCallResult { - tool, ok := handlers[toolCall.Name] - if !ok { - return nil - } - toolArgs, err := json.Marshal(toolCall.Args) - if err != nil { - return nil - } - result, err := tool(ctx, deps, toolArgs) - if err != nil { - return map[string]any{ - "error": err.Error(), - } - } - return result - }).WithAccumulator(&acc) - - err = stream.Pipe(w) - if err != nil { - // The client disppeared! - api.Logger.Error(ctx, "stream pipe error", "error", err) - return - } - - // acc.Messages() may sometimes return nil. Serializing this - // will cause a pq error: "cannot extract elements from a scalar". - newMessages := append([]aisdk.Message{}, acc.Messages()...) - if len(newMessages) > 0 { - raw, err := json.Marshal(newMessages) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to marshal chat message", - Detail: err.Error(), - }) - return - } - messages = append(messages, newMessages...) - - // Insert these messages into the database! - _, err = api.Database.InsertChatMessages(ctx, database.InsertChatMessagesParams{ - ChatID: chat.ID, - CreatedAt: dbtime.Now(), - Model: req.Model, - Provider: provider.Provider, - Content: raw, - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to insert chat messages", - Detail: err.Error(), - }) - return - } - } - - if acc.FinishReason() == aisdk.FinishReasonToolCalls { - continue - } - - break - } -} diff --git a/coderd/chat_test.go b/coderd/chat_test.go deleted file mode 100644 index 71e7b99ab3720..0000000000000 --- a/coderd/chat_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package coderd_test - -import ( - "net/http" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbgen" - "github.com/coder/coder/v2/coderd/database/dbtime" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/testutil" -) - -func TestChat(t *testing.T) { - t.Parallel() - - t.Run("ExperimentAgenticChatDisabled", func(t *testing.T) { - t.Parallel() - - client, _ := coderdtest.NewWithDatabase(t, nil) - owner := coderdtest.CreateFirstUser(t, client) - memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - - // Hit the endpoint to get the chat. It should return a 404. - ctx := testutil.Context(t, testutil.WaitShort) - _, err := memberClient.ListChats(ctx) - require.Error(t, err, "list chats should fail") - var sdkErr *codersdk.Error - require.ErrorAs(t, err, &sdkErr, "request should fail with an SDK error") - require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) - }) - - t.Run("ChatCRUD", func(t *testing.T) { - t.Parallel() - - dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAgenticChat)} - dv.AI.Value = codersdk.AIConfig{ - Providers: []codersdk.AIProviderConfig{ - { - Type: "fake", - APIKey: "", - BaseURL: "http://localhost", - Models: []string{"fake-model"}, - }, - }, - } - client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ - DeploymentValues: dv, - }) - owner := coderdtest.CreateFirstUser(t, client) - memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - - // Seed the database with some data. - dbChat := dbgen.Chat(t, db, database.Chat{ - OwnerID: memberUser.ID, - CreatedAt: dbtime.Now().Add(-time.Hour), - UpdatedAt: dbtime.Now().Add(-time.Hour), - Title: "This is a test chat", - }) - _ = dbgen.ChatMessage(t, db, database.ChatMessage{ - ChatID: dbChat.ID, - CreatedAt: dbtime.Now().Add(-time.Hour), - Content: []byte(`[{"content": "Hello world"}]`), - Model: "fake model", - Provider: "fake", - }) - - ctx := testutil.Context(t, testutil.WaitShort) - - // Listing chats should return the chat we just inserted. - chats, err := memberClient.ListChats(ctx) - require.NoError(t, err, "list chats should succeed") - require.Len(t, chats, 1, "response should have one chat") - require.Equal(t, dbChat.ID, chats[0].ID, "unexpected chat ID") - require.Equal(t, dbChat.Title, chats[0].Title, "unexpected chat title") - require.Equal(t, dbChat.CreatedAt.UTC(), chats[0].CreatedAt.UTC(), "unexpected chat created at") - require.Equal(t, dbChat.UpdatedAt.UTC(), chats[0].UpdatedAt.UTC(), "unexpected chat updated at") - - // Fetching a single chat by ID should return the same chat. - chat, err := memberClient.Chat(ctx, dbChat.ID) - require.NoError(t, err, "get chat should succeed") - require.Equal(t, chats[0], chat, "get chat should return the same chat") - - // Listing chat messages should return the message we just inserted. - messages, err := memberClient.ChatMessages(ctx, dbChat.ID) - require.NoError(t, err, "list chat messages should succeed") - require.Len(t, messages, 1, "response should have one message") - require.Equal(t, "Hello world", messages[0].Content, "response should have the correct message content") - - // Creating a new chat will fail because the model does not exist. - // TODO: Test the message streaming functionality with a mock model. - // Inserting a chat message will fail due to the model not existing. - _, err = memberClient.CreateChatMessage(ctx, dbChat.ID, codersdk.CreateChatMessageRequest{ - Model: "echo", - Message: codersdk.ChatMessage{ - Role: "user", - Content: "Hello world", - }, - Thinking: false, - }) - require.Error(t, err, "create chat message should fail") - var sdkErr *codersdk.Error - require.ErrorAs(t, err, &sdkErr, "create chat should fail with an SDK error") - require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode(), "create chat should fail with a 400 when model does not exist") - - // Creating a new chat message with malformed content should fail. - res, err := memberClient.Request(ctx, http.MethodPost, "/api/v2/chats/"+dbChat.ID.String()+"/messages", strings.NewReader(`{malformed json}`)) - require.NoError(t, err) - defer res.Body.Close() - apiErr := codersdk.ReadBodyAsError(res) - require.Contains(t, apiErr.Error(), "Failed to decode chat message") - - _, err = memberClient.CreateChat(ctx) - require.NoError(t, err, "create chat should succeed") - chats, err = memberClient.ListChats(ctx) - require.NoError(t, err, "list chats should succeed") - require.Len(t, chats, 2, "response should have two chats") - }) -} diff --git a/coderd/coderd.go b/coderd/coderd.go index 7c56355694995..519104a3723ae 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1890,7 +1890,7 @@ func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name strin mux := drpcmux.New() api.Logger.Debug(dialCtx, "starting in-memory AI bridge daemon", slog.F("name", name)) logger := api.Logger.Named(fmt.Sprintf("inmem-aibridged-%s", name)) - srv, err := aibridgedserver.NewServer(api.ctx) + srv, err := aibridgedserver.NewServer(api.ctx, api.Database) if err != nil { return nil, err } diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 40b1423a0f730..6129001f90ca9 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -16,6 +16,8 @@ import ( "golang.org/x/xerrors" "tailscale.com/tailcfg" + previewtypes "github.com/coder/preview/types" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" @@ -25,7 +27,6 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/tailnet" - previewtypes "github.com/coder/preview/types" ) // List is a helper function to reduce boilerplate when converting slices of diff --git a/codersdk/chat.go b/codersdk/chat.go deleted file mode 100644 index 2093adaff95e8..0000000000000 --- a/codersdk/chat.go +++ /dev/null @@ -1,153 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" - "golang.org/x/xerrors" -) - -// CreateChat creates a new chat. -func (c *Client) CreateChat(ctx context.Context) (Chat, error) { - res, err := c.Request(ctx, http.MethodPost, "/api/v2/chats", nil) - if err != nil { - return Chat{}, xerrors.Errorf("execute request: %w", err) - } - if res.StatusCode != http.StatusCreated { - return Chat{}, ReadBodyAsError(res) - } - defer res.Body.Close() - var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) -} - -type Chat struct { - ID uuid.UUID `json:"id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - UpdatedAt time.Time `json:"updated_at" format:"date-time"` - Title string `json:"title"` -} - -// ListChats lists all chats. -func (c *Client) ListChats(ctx context.Context) ([]Chat, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats", nil) - if err != nil { - return nil, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, ReadBodyAsError(res) - } - - var chats []Chat - return chats, json.NewDecoder(res.Body).Decode(&chats) -} - -// Chat returns a chat by ID. -func (c *Client) Chat(ctx context.Context, id uuid.UUID) (Chat, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s", id), nil) - if err != nil { - return Chat{}, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return Chat{}, ReadBodyAsError(res) - } - var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) -} - -// ChatMessages returns the messages of a chat. -func (c *Client) ChatMessages(ctx context.Context, id uuid.UUID) ([]ChatMessage, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/messages", id), nil) - if err != nil { - return nil, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, ReadBodyAsError(res) - } - var messages []ChatMessage - return messages, json.NewDecoder(res.Body).Decode(&messages) -} - -type ChatMessage = aisdk.Message - -type CreateChatMessageRequest struct { - Model string `json:"model"` - Message ChatMessage `json:"message"` - Thinking bool `json:"thinking"` -} - -// CreateChatMessage creates a new chat message and streams the response. -// If the provided message has a conflicting ID with an existing message, -// it will be overwritten. -func (c *Client) CreateChatMessage(ctx context.Context, id uuid.UUID, req CreateChatMessageRequest) (<-chan aisdk.DataStreamPart, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/messages", id), req) - defer func() { - if res != nil && res.Body != nil { - _ = res.Body.Close() - } - }() - if err != nil { - return nil, xerrors.Errorf("execute request: %w", err) - } - if res.StatusCode != http.StatusOK { - return nil, ReadBodyAsError(res) - } - nextEvent := ServerSentEventReader(ctx, res.Body) - - wc := make(chan aisdk.DataStreamPart, 256) - go func() { - defer close(wc) - defer res.Body.Close() - - for { - select { - case <-ctx.Done(): - return - default: - sse, err := nextEvent() - if err != nil { - return - } - if sse.Type != ServerSentEventTypeData { - continue - } - var part aisdk.DataStreamPart - b, ok := sse.Data.([]byte) - if !ok { - return - } - err = json.Unmarshal(b, &part) - if err != nil { - return - } - select { - case <-ctx.Done(): - return - case wc <- part: - } - } - } - }() - - return wc, nil -} - -func (c *Client) DeleteChat(ctx context.Context, id uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/chats/%s", id), nil) - if err != nil { - return xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) - } - return nil -} diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go deleted file mode 100644 index a2a31cf431fc1..0000000000000 --- a/codersdk/toolsdk/toolsdk.go +++ /dev/null @@ -1,1333 +0,0 @@ -package toolsdk - -import ( - "archive/tar" - "bytes" - "context" - "encoding/json" - "io" - - "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" - "golang.org/x/xerrors" - - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/agentsdk" -) - -func NewDeps(client *codersdk.Client, opts ...func(*Deps)) (Deps, error) { - d := Deps{ - coderClient: client, - } - for _, opt := range opts { - opt(&d) - } - // Allow nil client for unauthenticated operation - // This enables tools that don't require user authentication to function - return d, nil -} - -func WithAgentClient(client *agentsdk.Client) func(*Deps) { - return func(d *Deps) { - d.agentClient = client - } -} - -func WithAppStatusSlug(slug string) func(*Deps) { - return func(d *Deps) { - d.appStatusSlug = slug - } -} - -// Deps provides access to tool dependencies. -type Deps struct { - coderClient *codersdk.Client - agentClient *agentsdk.Client - appStatusSlug string -} - -// HandlerFunc is a typed function that handles a tool call. -type HandlerFunc[Arg, Ret any] func(context.Context, Deps, Arg) (Ret, error) - -// Tool consists of an aisdk.Tool and a corresponding typed handler function. -type Tool[Arg, Ret any] struct { - aisdk.Tool - Handler HandlerFunc[Arg, Ret] - - // UserClientOptional indicates whether this tool can function without a valid - // user authentication token. If true, the tool will be available even when - // running in an unauthenticated mode with just an agent token. - UserClientOptional bool -} - -// Generic returns a type-erased version of a TypedTool where the arguments and -// return values are converted to/from json.RawMessage. -// This allows the tool to be referenced without knowing the concrete arguments -// or return values. The original TypedHandlerFunc is wrapped to handle type -// conversion. -func (t Tool[Arg, Ret]) Generic() GenericTool { - return GenericTool{ - Tool: t.Tool, - UserClientOptional: t.UserClientOptional, - Handler: wrap(func(ctx context.Context, deps Deps, args json.RawMessage) (json.RawMessage, error) { - var typedArgs Arg - if err := json.Unmarshal(args, &typedArgs); err != nil { - return nil, xerrors.Errorf("failed to unmarshal args: %w", err) - } - ret, err := t.Handler(ctx, deps, typedArgs) - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(ret); err != nil { - return json.RawMessage{}, err - } - return buf.Bytes(), err - }, WithCleanContext, WithRecover), - } -} - -// GenericTool is a type-erased wrapper for GenericTool. -// This allows referencing the tool without knowing the concrete argument or -// return type. The Handler function allows calling the tool with known types. -type GenericTool struct { - aisdk.Tool - Handler GenericHandlerFunc - - // UserClientOptional indicates whether this tool can function without a valid - // user authentication token. If true, the tool will be available even when - // running in an unauthenticated mode with just an agent token. - UserClientOptional bool -} - -// GenericHandlerFunc is a function that handles a tool call. -type GenericHandlerFunc func(context.Context, Deps, json.RawMessage) (json.RawMessage, error) - -// NoArgs just represents an empty argument struct. -type NoArgs struct{} - -// WithRecover wraps a HandlerFunc to recover from panics and return an error. -func WithRecover(h GenericHandlerFunc) GenericHandlerFunc { - return func(ctx context.Context, deps Deps, args json.RawMessage) (ret json.RawMessage, err error) { - defer func() { - if r := recover(); r != nil { - err = xerrors.Errorf("tool handler panic: %v", r) - } - }() - return h(ctx, deps, args) - } -} - -// WithCleanContext wraps a HandlerFunc to provide it with a new context. -// This ensures that no data is passed using context.Value. -// If a deadline is set on the parent context, it will be passed to the child -// context. -func WithCleanContext(h GenericHandlerFunc) GenericHandlerFunc { - return func(parent context.Context, deps Deps, args json.RawMessage) (ret json.RawMessage, err error) { - child, childCancel := context.WithCancel(context.Background()) - defer childCancel() - // Ensure that the child context has the same deadline as the parent - // context. - if deadline, ok := parent.Deadline(); ok { - deadlineCtx, deadlineCancel := context.WithDeadline(child, deadline) - defer deadlineCancel() - child = deadlineCtx - } - // Ensure that cancellation propagates from the parent context to the child context. - go func() { - select { - case <-child.Done(): - return - case <-parent.Done(): - childCancel() - } - }() - return h(child, deps, args) - } -} - -// wrap wraps the provided GenericHandlerFunc with the provided middleware functions. -func wrap(hf GenericHandlerFunc, mw ...func(GenericHandlerFunc) GenericHandlerFunc) GenericHandlerFunc { - for _, m := range mw { - hf = m(hf) - } - return hf -} - -// All is a list of all tools that can be used in the Coder CLI. -// When you add a new tool, be sure to include it here! -var All = []GenericTool{ - CreateTemplate.Generic(), - CreateTemplateVersion.Generic(), - CreateWorkspace.Generic(), - CreateWorkspaceBuild.Generic(), - DeleteTemplate.Generic(), - ListTemplates.Generic(), - ListTemplateVersionParameters.Generic(), - ListWorkspaces.Generic(), - GetAuthenticatedUser.Generic(), - GetTemplateVersionLogs.Generic(), - GetWorkspace.Generic(), - GetWorkspaceAgentLogs.Generic(), - GetWorkspaceBuildLogs.Generic(), - ReportTask.Generic(), - UploadTarFile.Generic(), - UpdateTemplateActiveVersion.Generic(), -} - -type ReportTaskArgs struct { - Link string `json:"link"` - State string `json:"state"` - Summary string `json:"summary"` -} - -var ReportTask = Tool[ReportTaskArgs, codersdk.Response]{ - Tool: aisdk.Tool{ - Name: "coder_report_task", - Description: `Report progress on your work. - -The user observes your work through a Task UI. To keep them updated -on your progress, or if you need help - use this tool. - -Good Tasks -- "Cloning the repository " -- "Working on " -- "Figuring our why is happening" - -Bad Tasks -- "I'm working on it" -- "I'm trying to fix it" -- "I'm trying to implement " - -Use the "state" field to indicate your progress. Periodically report -progress with state "working" to keep the user updated. It is not possible to send too many updates! - -ONLY report a "complete" or "failure" state if you have FULLY completed the task. -`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "summary": map[string]any{ - "type": "string", - "description": "A concise summary of your current progress on the task. This must be less than 160 characters in length.", - }, - "link": map[string]any{ - "type": "string", - "description": "A link to a relevant resource, such as a PR or issue.", - }, - "state": map[string]any{ - "type": "string", - "description": "The state of your task. This can be one of the following: working, complete, or failure. Select the state that best represents your current progress.", - "enum": []string{ - string(codersdk.WorkspaceAppStatusStateWorking), - string(codersdk.WorkspaceAppStatusStateComplete), - string(codersdk.WorkspaceAppStatusStateFailure), - }, - }, - }, - Required: []string{"summary", "link", "state"}, - }, - }, - UserClientOptional: true, - Handler: func(ctx context.Context, deps Deps, args ReportTaskArgs) (codersdk.Response, error) { - if deps.agentClient == nil { - return codersdk.Response{}, xerrors.New("tool unavailable as CODER_AGENT_TOKEN or CODER_AGENT_TOKEN_FILE not set") - } - if deps.appStatusSlug == "" { - return codersdk.Response{}, xerrors.New("tool unavailable as CODER_MCP_APP_STATUS_SLUG is not set") - } - if len(args.Summary) > 160 { - return codersdk.Response{}, xerrors.New("summary must be less than 160 characters") - } - if err := deps.agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{ - AppSlug: deps.appStatusSlug, - Message: args.Summary, - URI: args.Link, - State: codersdk.WorkspaceAppStatusState(args.State), - }); err != nil { - return codersdk.Response{}, err - } - return codersdk.Response{ - Message: "Thanks for reporting!", - }, nil - }, -} - -type GetWorkspaceArgs struct { - WorkspaceID string `json:"workspace_id"` -} - -var GetWorkspace = Tool[GetWorkspaceArgs, codersdk.Workspace]{ - Tool: aisdk.Tool{ - Name: "coder_get_workspace", - Description: `Get a workspace by ID. - -This returns more data than list_workspaces to reduce token usage.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "workspace_id": map[string]any{ - "type": "string", - }, - }, - Required: []string{"workspace_id"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args GetWorkspaceArgs) (codersdk.Workspace, error) { - wsID, err := uuid.Parse(args.WorkspaceID) - if err != nil { - return codersdk.Workspace{}, xerrors.New("workspace_id must be a valid UUID") - } - return deps.coderClient.Workspace(ctx, wsID) - }, -} - -type CreateWorkspaceArgs struct { - Name string `json:"name"` - RichParameters map[string]string `json:"rich_parameters"` - TemplateVersionID string `json:"template_version_id"` - User string `json:"user"` -} - -var CreateWorkspace = Tool[CreateWorkspaceArgs, codersdk.Workspace]{ - Tool: aisdk.Tool{ - Name: "coder_create_workspace", - Description: `Create a new workspace in Coder. - -If a user is asking to "test a template", they are typically referring -to creating a workspace from a template to ensure the infrastructure -is provisioned correctly and the agent can connect to the control plane. -`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "user": map[string]any{ - "type": "string", - "description": "Username or ID of the user to create the workspace for. Use the `me` keyword to create a workspace for the authenticated user.", - }, - "template_version_id": map[string]any{ - "type": "string", - "description": "ID of the template version to create the workspace from.", - }, - "name": map[string]any{ - "type": "string", - "description": "Name of the workspace to create.", - }, - "rich_parameters": map[string]any{ - "type": "object", - "description": "Key/value pairs of rich parameters to pass to the template version to create the workspace.", - }, - }, - Required: []string{"user", "template_version_id", "name", "rich_parameters"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args CreateWorkspaceArgs) (codersdk.Workspace, error) { - tvID, err := uuid.Parse(args.TemplateVersionID) - if err != nil { - return codersdk.Workspace{}, xerrors.New("template_version_id must be a valid UUID") - } - if args.User == "" { - args.User = codersdk.Me - } - var buildParams []codersdk.WorkspaceBuildParameter - for k, v := range args.RichParameters { - buildParams = append(buildParams, codersdk.WorkspaceBuildParameter{ - Name: k, - Value: v, - }) - } - workspace, err := deps.coderClient.CreateUserWorkspace(ctx, args.User, codersdk.CreateWorkspaceRequest{ - TemplateVersionID: tvID, - Name: args.Name, - RichParameterValues: buildParams, - }) - if err != nil { - return codersdk.Workspace{}, err - } - return workspace, nil - }, -} - -type ListWorkspacesArgs struct { - Owner string `json:"owner"` -} - -var ListWorkspaces = Tool[ListWorkspacesArgs, []MinimalWorkspace]{ - Tool: aisdk.Tool{ - Name: "coder_list_workspaces", - Description: "Lists workspaces for the authenticated user.", - Schema: aisdk.Schema{ - Properties: map[string]any{ - "owner": map[string]any{ - "type": "string", - "description": "The owner of the workspaces to list. Use \"me\" to list workspaces for the authenticated user. If you do not specify an owner, \"me\" will be assumed by default.", - }, - }, - Required: []string{}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args ListWorkspacesArgs) ([]MinimalWorkspace, error) { - owner := args.Owner - if owner == "" { - owner = codersdk.Me - } - workspaces, err := deps.coderClient.Workspaces(ctx, codersdk.WorkspaceFilter{ - Owner: owner, - }) - if err != nil { - return nil, err - } - minimalWorkspaces := make([]MinimalWorkspace, len(workspaces.Workspaces)) - for i, workspace := range workspaces.Workspaces { - minimalWorkspaces[i] = MinimalWorkspace{ - ID: workspace.ID.String(), - Name: workspace.Name, - TemplateID: workspace.TemplateID.String(), - TemplateName: workspace.TemplateName, - TemplateDisplayName: workspace.TemplateDisplayName, - TemplateIcon: workspace.TemplateIcon, - TemplateActiveVersionID: workspace.TemplateActiveVersionID, - Outdated: workspace.Outdated, - } - } - return minimalWorkspaces, nil - }, -} - -var ListTemplates = Tool[NoArgs, []MinimalTemplate]{ - Tool: aisdk.Tool{ - Name: "coder_list_templates", - Description: "Lists templates for the authenticated user.", - Schema: aisdk.Schema{ - Properties: map[string]any{}, - Required: []string{}, - }, - }, - Handler: func(ctx context.Context, deps Deps, _ NoArgs) ([]MinimalTemplate, error) { - templates, err := deps.coderClient.Templates(ctx, codersdk.TemplateFilter{}) - if err != nil { - return nil, err - } - minimalTemplates := make([]MinimalTemplate, len(templates)) - for i, template := range templates { - minimalTemplates[i] = MinimalTemplate{ - DisplayName: template.DisplayName, - ID: template.ID.String(), - Name: template.Name, - Description: template.Description, - ActiveVersionID: template.ActiveVersionID, - ActiveUserCount: template.ActiveUserCount, - } - } - return minimalTemplates, nil - }, -} - -type ListTemplateVersionParametersArgs struct { - TemplateVersionID string `json:"template_version_id"` -} - -var ListTemplateVersionParameters = Tool[ListTemplateVersionParametersArgs, []codersdk.TemplateVersionParameter]{ - Tool: aisdk.Tool{ - Name: "coder_template_version_parameters", - Description: "Get the parameters for a template version. You can refer to these as workspace parameters to the user, as they are typically important for creating a workspace.", - Schema: aisdk.Schema{ - Properties: map[string]any{ - "template_version_id": map[string]any{ - "type": "string", - }, - }, - Required: []string{"template_version_id"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args ListTemplateVersionParametersArgs) ([]codersdk.TemplateVersionParameter, error) { - templateVersionID, err := uuid.Parse(args.TemplateVersionID) - if err != nil { - return nil, xerrors.Errorf("template_version_id must be a valid UUID: %w", err) - } - parameters, err := deps.coderClient.TemplateVersionRichParameters(ctx, templateVersionID) - if err != nil { - return nil, err - } - return parameters, nil - }, -} - -var GetAuthenticatedUser = Tool[NoArgs, codersdk.User]{ - Tool: aisdk.Tool{ - Name: "coder_get_authenticated_user", - Description: "Get the currently authenticated user, similar to the `whoami` command.", - Schema: aisdk.Schema{ - Properties: map[string]any{}, - Required: []string{}, - }, - }, - Handler: func(ctx context.Context, deps Deps, _ NoArgs) (codersdk.User, error) { - return deps.coderClient.User(ctx, "me") - }, -} - -type CreateWorkspaceBuildArgs struct { - TemplateVersionID string `json:"template_version_id"` - Transition string `json:"transition"` - WorkspaceID string `json:"workspace_id"` -} - -var CreateWorkspaceBuild = Tool[CreateWorkspaceBuildArgs, codersdk.WorkspaceBuild]{ - Tool: aisdk.Tool{ - Name: "coder_create_workspace_build", - Description: "Create a new workspace build for an existing workspace. Use this to start, stop, or delete.", - Schema: aisdk.Schema{ - Properties: map[string]any{ - "workspace_id": map[string]any{ - "type": "string", - }, - "transition": map[string]any{ - "type": "string", - "description": "The transition to perform. Must be one of: start, stop, delete", - "enum": []string{"start", "stop", "delete"}, - }, - "template_version_id": map[string]any{ - "type": "string", - "description": "(Optional) The template version ID to use for the workspace build. If not provided, the previously built version will be used.", - }, - }, - Required: []string{"workspace_id", "transition"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args CreateWorkspaceBuildArgs) (codersdk.WorkspaceBuild, error) { - workspaceID, err := uuid.Parse(args.WorkspaceID) - if err != nil { - return codersdk.WorkspaceBuild{}, xerrors.Errorf("workspace_id must be a valid UUID: %w", err) - } - var templateVersionID uuid.UUID - if args.TemplateVersionID != "" { - tvID, err := uuid.Parse(args.TemplateVersionID) - if err != nil { - return codersdk.WorkspaceBuild{}, xerrors.Errorf("template_version_id must be a valid UUID: %w", err) - } - templateVersionID = tvID - } - cbr := codersdk.CreateWorkspaceBuildRequest{ - Transition: codersdk.WorkspaceTransition(args.Transition), - } - if templateVersionID != uuid.Nil { - cbr.TemplateVersionID = templateVersionID - } - return deps.coderClient.CreateWorkspaceBuild(ctx, workspaceID, cbr) - }, -} - -type CreateTemplateVersionArgs struct { - FileID string `json:"file_id"` - TemplateID string `json:"template_id"` -} - -var CreateTemplateVersion = Tool[CreateTemplateVersionArgs, codersdk.TemplateVersion]{ - Tool: aisdk.Tool{ - Name: "coder_create_template_version", - Description: `Create a new template version. This is a precursor to creating a template, or you can update an existing template. - -Templates are Terraform defining a development environment. The provisioned infrastructure must run -an Agent that connects to the Coder Control Plane to provide a rich experience. - -Here are some strict rules for creating a template version: -- YOU MUST NOT use "variable" or "output" blocks in the Terraform code. -- YOU MUST ALWAYS check template version logs after creation to ensure the template was imported successfully. - -When a template version is created, a Terraform Plan occurs that ensures the infrastructure -_could_ be provisioned, but actual provisioning occurs when a workspace is created. - - -The Coder Terraform Provider can be imported like: - -` + "```" + `hcl -terraform { - required_providers { - coder = { - source = "coder/coder" - } - } -} -` + "```" + ` - -A destroy does not occur when a user stops a workspace, but rather the transition changes: - -` + "```" + `hcl -data "coder_workspace" "me" {} -` + "```" + ` - -This data source provides the following fields: -- id: The UUID of the workspace. -- name: The name of the workspace. -- transition: Either "start" or "stop". -- start_count: A computed count based on the transition field. If "start", this will be 1. - -Access workspace owner information with: - -` + "```" + `hcl -data "coder_workspace_owner" "me" {} -` + "```" + ` - -This data source provides the following fields: -- id: The UUID of the workspace owner. -- name: The name of the workspace owner. -- full_name: The full name of the workspace owner. -- email: The email of the workspace owner. -- session_token: A token that can be used to authenticate the workspace owner. It is regenerated every time the workspace is started. -- oidc_access_token: A valid OpenID Connect access token of the workspace owner. This is only available if the workspace owner authenticated with OpenID Connect. If a valid token cannot be obtained, this value will be an empty string. - -Parameters are defined in the template version. They are rendered in the UI on the workspace creation page: - -` + "```" + `hcl -resource "coder_parameter" "region" { - name = "region" - type = "string" - default = "us-east-1" -} -` + "```" + ` - -This resource accepts the following properties: -- name: The name of the parameter. -- default: The default value of the parameter. -- type: The type of the parameter. Must be one of: "string", "number", "bool", or "list(string)". -- display_name: The displayed name of the parameter as it will appear in the UI. -- description: The description of the parameter as it will appear in the UI. -- ephemeral: The value of an ephemeral parameter will not be preserved between consecutive workspace builds. -- form_type: The type of this parameter. Must be one of: [radio, slider, input, dropdown, checkbox, switch, multi-select, tag-select, textarea, error]. -- icon: A URL to an icon to display in the UI. -- mutable: Whether this value can be changed after workspace creation. This can be destructive for values like region, so use with caution! -- option: Each option block defines a value for a user to select from. (see below for nested schema) - Required: - - name: The name of the option. - - value: The value of the option. - Optional: - - description: The description of the option as it will appear in the UI. - - icon: A URL to an icon to display in the UI. - -A Workspace Agent runs on provisioned infrastructure to provide access to the workspace: - -` + "```" + `hcl -resource "coder_agent" "dev" { - arch = "amd64" - os = "linux" -} -` + "```" + ` - -This resource accepts the following properties: -- arch: The architecture of the agent. Must be one of: "amd64", "arm64", or "armv7". -- os: The operating system of the agent. Must be one of: "linux", "windows", or "darwin". -- auth: The authentication method for the agent. Must be one of: "token", "google-instance-identity", "aws-instance-identity", or "azure-instance-identity". It is insecure to pass the agent token via exposed variables to Virtual Machines. Instance Identity enables provisioned VMs to authenticate by instance ID on start. -- dir: The starting directory when a user creates a shell session. Defaults to "$HOME". -- env: A map of environment variables to set for the agent. -- startup_script: A script to run after the agent starts. This script MUST exit eventually to signal that startup has completed. Use "&" or "screen" to run processes in the background. - -This resource provides the following fields: -- id: The UUID of the agent. -- init_script: The script to run on provisioned infrastructure to fetch and start the agent. -- token: Set the environment variable CODER_AGENT_TOKEN to this value to authenticate the agent. - -The agent MUST be installed and started using the init_script. A utility like curl or wget to fetch the agent binary must exist in the provisioned infrastructure. - -Expose terminal or HTTP applications running in a workspace with: - -` + "```" + `hcl -resource "coder_app" "dev" { - agent_id = coder_agent.dev.id - slug = "my-app-name" - display_name = "My App" - icon = "https://my-app.com/icon.svg" - url = "http://127.0.0.1:3000" -} -` + "```" + ` - -This resource accepts the following properties: -- agent_id: The ID of the agent to attach the app to. -- slug: The slug of the app. -- display_name: The displayed name of the app as it will appear in the UI. -- icon: A URL to an icon to display in the UI. -- url: An external url if external=true or a URL to be proxied to from inside the workspace. This should be of the form http://localhost:PORT[/SUBPATH]. Either command or url may be specified, but not both. -- command: A command to run in a terminal opening this app. In the web, this will open in a new tab. In the CLI, this will SSH and execute the command. Either command or url may be specified, but not both. -- external: Whether this app is an external app. If true, the url will be opened in a new tab. - - -The Coder Server may not be authenticated with the infrastructure provider a user requests. In this scenario, -the user will need to provide credentials to the Coder Server before the workspace can be provisioned. - -Here are examples of provisioning the Coder Agent on specific infrastructure providers: - - -// The agent is configured with "aws-instance-identity" auth. -terraform { - required_providers { - cloudinit = { - source = "hashicorp/cloudinit" - } - aws = { - source = "hashicorp/aws" - } - } -} - -data "cloudinit_config" "user_data" { - gzip = false - base64_encode = false - boundary = "//" - part { - filename = "cloud-config.yaml" - content_type = "text/cloud-config" - - // Here is the content of the cloud-config.yaml.tftpl file: - // #cloud-config - // cloud_final_modules: - // - [scripts-user, always] - // hostname: ${hostname} - // users: - // - name: ${linux_user} - // sudo: ALL=(ALL) NOPASSWD:ALL - // shell: /bin/bash - content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { - hostname = local.hostname - linux_user = local.linux_user - }) - } - - part { - filename = "userdata.sh" - content_type = "text/x-shellscript" - - // Here is the content of the userdata.sh.tftpl file: - // #!/bin/bash - // sudo -u '${linux_user}' sh -c '${init_script}' - content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", { - linux_user = local.linux_user - - init_script = try(coder_agent.dev[0].init_script, "") - }) - } -} - -resource "aws_instance" "dev" { - ami = data.aws_ami.ubuntu.id - availability_zone = "${data.coder_parameter.region.value}a" - instance_type = data.coder_parameter.instance_type.value - - user_data = data.cloudinit_config.user_data.rendered - tags = { - Name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" - } - lifecycle { - ignore_changes = [ami] - } -} - - - -// The agent is configured with "google-instance-identity" auth. -terraform { - required_providers { - google = { - source = "hashicorp/google" - } - } -} - -resource "google_compute_instance" "dev" { - zone = module.gcp_region.value - count = data.coder_workspace.me.start_count - name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-root" - machine_type = "e2-medium" - network_interface { - network = "default" - access_config { - // Ephemeral public IP - } - } - boot_disk { - auto_delete = false - source = google_compute_disk.root.name - } - // In order to use google-instance-identity, a service account *must* be provided. - service_account { - email = data.google_compute_default_service_account.default.email - scopes = ["cloud-platform"] - } - # ONLY FOR WINDOWS: - # metadata = { - # windows-startup-script-ps1 = coder_agent.main.init_script - # } - # The startup script runs as root with no $HOME environment set up, so instead of directly - # running the agent init script, create a user (with a homedir, default shell and sudo - # permissions) and execute the init script as that user. - # - # The agent MUST be started in here. - metadata_startup_script = </dev/null 2>&1; then - useradd -m -s /bin/bash "${local.linux_user}" - echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user -fi - -exec sudo -u "${local.linux_user}" sh -c '${coder_agent.main.init_script}' -EOMETA -} - - - -// The agent is configured with "azure-instance-identity" auth. -terraform { - required_providers { - azurerm = { - source = "hashicorp/azurerm" - } - cloudinit = { - source = "hashicorp/cloudinit" - } - } -} - -data "cloudinit_config" "user_data" { - gzip = false - base64_encode = true - - boundary = "//" - - part { - filename = "cloud-config.yaml" - content_type = "text/cloud-config" - - // Here is the content of the cloud-config.yaml.tftpl file: - // #cloud-config - // cloud_final_modules: - // - [scripts-user, always] - // bootcmd: - // # work around https://github.com/hashicorp/terraform-provider-azurerm/issues/6117 - // - until [ -e /dev/disk/azure/scsi1/lun10 ]; do sleep 1; done - // device_aliases: - // homedir: /dev/disk/azure/scsi1/lun10 - // disk_setup: - // homedir: - // table_type: gpt - // layout: true - // fs_setup: - // - label: coder_home - // filesystem: ext4 - // device: homedir.1 - // mounts: - // - ["LABEL=coder_home", "/home/${username}"] - // hostname: ${hostname} - // users: - // - name: ${username} - // sudo: ["ALL=(ALL) NOPASSWD:ALL"] - // groups: sudo - // shell: /bin/bash - // packages: - // - git - // write_files: - // - path: /opt/coder/init - // permissions: "0755" - // encoding: b64 - // content: ${init_script} - // - path: /etc/systemd/system/coder-agent.service - // permissions: "0644" - // content: | - // [Unit] - // Description=Coder Agent - // After=network-online.target - // Wants=network-online.target - - // [Service] - // User=${username} - // ExecStart=/opt/coder/init - // Restart=always - // RestartSec=10 - // TimeoutStopSec=90 - // KillMode=process - - // OOMScoreAdjust=-900 - // SyslogIdentifier=coder-agent - - // [Install] - // WantedBy=multi-user.target - // runcmd: - // - chown ${username}:${username} /home/${username} - // - systemctl enable coder-agent - // - systemctl start coder-agent - content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { - username = "coder" # Ensure this user/group does not exist in your VM image - init_script = base64encode(coder_agent.main.init_script) - hostname = lower(data.coder_workspace.me.name) - }) - } -} - -resource "azurerm_linux_virtual_machine" "main" { - count = data.coder_workspace.me.start_count - name = "vm" - resource_group_name = azurerm_resource_group.main.name - location = azurerm_resource_group.main.location - size = data.coder_parameter.instance_type.value - // cloud-init overwrites this, so the value here doesn't matter - admin_username = "adminuser" - admin_ssh_key { - public_key = tls_private_key.dummy.public_key_openssh - username = "adminuser" - } - - network_interface_ids = [ - azurerm_network_interface.main.id, - ] - computer_name = lower(data.coder_workspace.me.name) - os_disk { - caching = "ReadWrite" - storage_account_type = "Standard_LRS" - } - source_image_reference { - publisher = "Canonical" - offer = "0001-com-ubuntu-server-focal" - sku = "20_04-lts-gen2" - version = "latest" - } - user_data = data.cloudinit_config.user_data.rendered -} - - - -terraform { - required_providers { - coder = { - source = "kreuzwerker/docker" - } - } -} - -// The agent is configured with "token" auth. - -resource "docker_container" "workspace" { - count = data.coder_workspace.me.start_count - image = "codercom/enterprise-base:ubuntu" - # Uses lower() to avoid Docker restriction on container names. - name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" - # Hostname makes the shell more user friendly: coder@my-workspace:~$ - hostname = data.coder_workspace.me.name - # Use the docker gateway if the access URL is 127.0.0.1. - entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")] - env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"] - host { - host = "host.docker.internal" - ip = "host-gateway" - } - volumes { - container_path = "/home/coder" - volume_name = docker_volume.home_volume.name - read_only = false - } -} - - - -// The agent is configured with "token" auth. - -resource "kubernetes_deployment" "main" { - count = data.coder_workspace.me.start_count - depends_on = [ - kubernetes_persistent_volume_claim.home - ] - wait_for_rollout = false - metadata { - name = "coder-${data.coder_workspace.me.id}" - } - - spec { - replicas = 1 - strategy { - type = "Recreate" - } - - template { - spec { - security_context { - run_as_user = 1000 - fs_group = 1000 - run_as_non_root = true - } - - container { - name = "dev" - image = "codercom/enterprise-base:ubuntu" - image_pull_policy = "Always" - command = ["sh", "-c", coder_agent.main.init_script] - security_context { - run_as_user = "1000" - } - env { - name = "CODER_AGENT_TOKEN" - value = coder_agent.main.token - } - } - } - } - } -} - - -The file_id provided is a reference to a tar file you have uploaded containing the Terraform. -`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "template_id": map[string]any{ - "type": "string", - }, - "file_id": map[string]any{ - "type": "string", - }, - }, - Required: []string{"file_id"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args CreateTemplateVersionArgs) (codersdk.TemplateVersion, error) { - me, err := deps.coderClient.User(ctx, "me") - if err != nil { - return codersdk.TemplateVersion{}, err - } - fileID, err := uuid.Parse(args.FileID) - if err != nil { - return codersdk.TemplateVersion{}, xerrors.Errorf("file_id must be a valid UUID: %w", err) - } - var templateID uuid.UUID - if args.TemplateID != "" { - tid, err := uuid.Parse(args.TemplateID) - if err != nil { - return codersdk.TemplateVersion{}, xerrors.Errorf("template_id must be a valid UUID: %w", err) - } - templateID = tid - } - templateVersion, err := deps.coderClient.CreateTemplateVersion(ctx, me.OrganizationIDs[0], codersdk.CreateTemplateVersionRequest{ - Message: "Created by AI", - StorageMethod: codersdk.ProvisionerStorageMethodFile, - FileID: fileID, - Provisioner: codersdk.ProvisionerTypeTerraform, - TemplateID: templateID, - }) - if err != nil { - return codersdk.TemplateVersion{}, err - } - return templateVersion, nil - }, -} - -type GetWorkspaceAgentLogsArgs struct { - WorkspaceAgentID string `json:"workspace_agent_id"` -} - -var GetWorkspaceAgentLogs = Tool[GetWorkspaceAgentLogsArgs, []string]{ - Tool: aisdk.Tool{ - Name: "coder_get_workspace_agent_logs", - Description: `Get the logs of a workspace agent. - - More logs may appear after this call. It does not wait for the agent to finish.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "workspace_agent_id": map[string]any{ - "type": "string", - }, - }, - Required: []string{"workspace_agent_id"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args GetWorkspaceAgentLogsArgs) ([]string, error) { - workspaceAgentID, err := uuid.Parse(args.WorkspaceAgentID) - if err != nil { - return nil, xerrors.Errorf("workspace_agent_id must be a valid UUID: %w", err) - } - logs, closer, err := deps.coderClient.WorkspaceAgentLogsAfter(ctx, workspaceAgentID, 0, false) - if err != nil { - return nil, err - } - defer closer.Close() - var acc []string - for logChunk := range logs { - for _, log := range logChunk { - acc = append(acc, log.Output) - } - } - return acc, nil - }, -} - -type GetWorkspaceBuildLogsArgs struct { - WorkspaceBuildID string `json:"workspace_build_id"` -} - -var GetWorkspaceBuildLogs = Tool[GetWorkspaceBuildLogsArgs, []string]{ - Tool: aisdk.Tool{ - Name: "coder_get_workspace_build_logs", - Description: `Get the logs of a workspace build. - - Useful for checking whether a workspace builds successfully or not.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "workspace_build_id": map[string]any{ - "type": "string", - }, - }, - Required: []string{"workspace_build_id"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args GetWorkspaceBuildLogsArgs) ([]string, error) { - workspaceBuildID, err := uuid.Parse(args.WorkspaceBuildID) - if err != nil { - return nil, xerrors.Errorf("workspace_build_id must be a valid UUID: %w", err) - } - logs, closer, err := deps.coderClient.WorkspaceBuildLogsAfter(ctx, workspaceBuildID, 0) - if err != nil { - return nil, err - } - defer closer.Close() - var acc []string - for log := range logs { - acc = append(acc, log.Output) - } - return acc, nil - }, -} - -type GetTemplateVersionLogsArgs struct { - TemplateVersionID string `json:"template_version_id"` -} - -var GetTemplateVersionLogs = Tool[GetTemplateVersionLogsArgs, []string]{ - Tool: aisdk.Tool{ - Name: "coder_get_template_version_logs", - Description: "Get the logs of a template version. This is useful to check whether a template version successfully imports or not.", - Schema: aisdk.Schema{ - Properties: map[string]any{ - "template_version_id": map[string]any{ - "type": "string", - }, - }, - Required: []string{"template_version_id"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args GetTemplateVersionLogsArgs) ([]string, error) { - templateVersionID, err := uuid.Parse(args.TemplateVersionID) - if err != nil { - return nil, xerrors.Errorf("template_version_id must be a valid UUID: %w", err) - } - - logs, closer, err := deps.coderClient.TemplateVersionLogsAfter(ctx, templateVersionID, 0) - if err != nil { - return nil, err - } - defer closer.Close() - var acc []string - for log := range logs { - acc = append(acc, log.Output) - } - return acc, nil - }, -} - -type UpdateTemplateActiveVersionArgs struct { - TemplateID string `json:"template_id"` - TemplateVersionID string `json:"template_version_id"` -} - -var UpdateTemplateActiveVersion = Tool[UpdateTemplateActiveVersionArgs, string]{ - Tool: aisdk.Tool{ - Name: "coder_update_template_active_version", - Description: "Update the active version of a template. This is helpful when iterating on templates.", - Schema: aisdk.Schema{ - Properties: map[string]any{ - "template_id": map[string]any{ - "type": "string", - }, - "template_version_id": map[string]any{ - "type": "string", - }, - }, - Required: []string{"template_id", "template_version_id"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args UpdateTemplateActiveVersionArgs) (string, error) { - templateID, err := uuid.Parse(args.TemplateID) - if err != nil { - return "", xerrors.Errorf("template_id must be a valid UUID: %w", err) - } - templateVersionID, err := uuid.Parse(args.TemplateVersionID) - if err != nil { - return "", xerrors.Errorf("template_version_id must be a valid UUID: %w", err) - } - err = deps.coderClient.UpdateActiveTemplateVersion(ctx, templateID, codersdk.UpdateActiveTemplateVersion{ - ID: templateVersionID, - }) - if err != nil { - return "", err - } - return "Successfully updated active version!", nil - }, -} - -type UploadTarFileArgs struct { - Files map[string]string `json:"files"` -} - -var UploadTarFile = Tool[UploadTarFileArgs, codersdk.UploadResponse]{ - Tool: aisdk.Tool{ - Name: "coder_upload_tar_file", - Description: `Create and upload a tar file by key/value mapping of file names to file contents. Use this to create template versions. Reference the tool description of "create_template_version" to understand template requirements.`, - Schema: aisdk.Schema{ - Properties: map[string]any{ - "files": map[string]any{ - "type": "object", - "description": "A map of file names to file contents.", - }, - }, - Required: []string{"files"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args UploadTarFileArgs) (codersdk.UploadResponse, error) { - pipeReader, pipeWriter := io.Pipe() - done := make(chan struct{}) - go func() { - defer func() { - _ = pipeWriter.Close() - close(done) - }() - tarWriter := tar.NewWriter(pipeWriter) - for name, content := range args.Files { - header := &tar.Header{ - Name: name, - Size: int64(len(content)), - Mode: 0o644, - } - if err := tarWriter.WriteHeader(header); err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - if _, err := tarWriter.Write([]byte(content)); err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - } - if err := tarWriter.Close(); err != nil { - _ = pipeWriter.CloseWithError(err) - } - }() - - resp, err := deps.coderClient.Upload(ctx, codersdk.ContentTypeTar, pipeReader) - if err != nil { - _ = pipeReader.CloseWithError(err) - <-done - return codersdk.UploadResponse{}, err - } - <-done - return resp, nil - }, -} - -type CreateTemplateArgs struct { - Description string `json:"description"` - DisplayName string `json:"display_name"` - Icon string `json:"icon"` - Name string `json:"name"` - VersionID string `json:"version_id"` -} - -var CreateTemplate = Tool[CreateTemplateArgs, codersdk.Template]{ - Tool: aisdk.Tool{ - Name: "coder_create_template", - Description: "Create a new template in Coder. First, you must create a template version.", - Schema: aisdk.Schema{ - Properties: map[string]any{ - "name": map[string]any{ - "type": "string", - }, - "display_name": map[string]any{ - "type": "string", - }, - "description": map[string]any{ - "type": "string", - }, - "icon": map[string]any{ - "type": "string", - "description": "A URL to an icon to use.", - }, - "version_id": map[string]any{ - "type": "string", - "description": "The ID of the version to use.", - }, - }, - Required: []string{"name", "display_name", "description", "version_id"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args CreateTemplateArgs) (codersdk.Template, error) { - me, err := deps.coderClient.User(ctx, "me") - if err != nil { - return codersdk.Template{}, err - } - versionID, err := uuid.Parse(args.VersionID) - if err != nil { - return codersdk.Template{}, xerrors.Errorf("version_id must be a valid UUID: %w", err) - } - template, err := deps.coderClient.CreateTemplate(ctx, me.OrganizationIDs[0], codersdk.CreateTemplateRequest{ - Name: args.Name, - DisplayName: args.DisplayName, - Description: args.Description, - VersionID: versionID, - }) - if err != nil { - return codersdk.Template{}, err - } - return template, nil - }, -} - -type DeleteTemplateArgs struct { - TemplateID string `json:"template_id"` -} - -var DeleteTemplate = Tool[DeleteTemplateArgs, codersdk.Response]{ - Tool: aisdk.Tool{ - Name: "coder_delete_template", - Description: "Delete a template. This is irreversible.", - Schema: aisdk.Schema{ - Properties: map[string]any{ - "template_id": map[string]any{ - "type": "string", - }, - }, - Required: []string{"template_id"}, - }, - }, - Handler: func(ctx context.Context, deps Deps, args DeleteTemplateArgs) (codersdk.Response, error) { - templateID, err := uuid.Parse(args.TemplateID) - if err != nil { - return codersdk.Response{}, xerrors.Errorf("template_id must be a valid UUID: %w", err) - } - err = deps.coderClient.DeleteTemplate(ctx, templateID) - if err != nil { - return codersdk.Response{}, err - } - return codersdk.Response{ - Message: "Template deleted successfully.", - }, nil - }, -} - -type MinimalWorkspace struct { - ID string `json:"id"` - Name string `json:"name"` - TemplateID string `json:"template_id"` - TemplateName string `json:"template_name"` - TemplateDisplayName string `json:"template_display_name"` - TemplateIcon string `json:"template_icon"` - TemplateActiveVersionID uuid.UUID `json:"template_active_version_id"` - Outdated bool `json:"outdated"` -} - -type MinimalTemplate struct { - DisplayName string `json:"display_name"` - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - ActiveVersionID uuid.UUID `json:"active_version_id"` - ActiveUserCount int `json:"active_user_count"` -} diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go deleted file mode 100644 index f9c35dba5951d..0000000000000 --- a/codersdk/toolsdk/toolsdk_test.go +++ /dev/null @@ -1,607 +0,0 @@ -package toolsdk_test - -import ( - "context" - "encoding/json" - "os" - "sort" - "sync" - "testing" - "time" - - "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/goleak" - - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbfake" - "github.com/coder/coder/v2/coderd/database/dbgen" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/coder/coder/v2/codersdk/toolsdk" - "github.com/coder/coder/v2/provisionersdk/proto" - "github.com/coder/coder/v2/testutil" -) - -// These tests are dependent on the state of the coder server. -// Running them in parallel is prone to racy behavior. -// nolint:tparallel,paralleltest -func TestTools(t *testing.T) { - // Given: a running coderd instance - setupCtx := testutil.Context(t, testutil.WaitShort) - client, store := coderdtest.NewWithDatabase(t, nil) - owner := coderdtest.CreateFirstUser(t, client) - // Given: a member user with which to test the tools. - memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - // Given: a workspace with an agent. - // nolint:gocritic // This is in a test package and does not end up in the build - r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { - agents[0].Apps = []*proto.App{ - { - Slug: "some-agent-app", - }, - } - return agents - }).Do() - - // Given: a client configured with the agent token. - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(r.AgentToken) - // Get the agent ID from the API. Overriding it in dbfake doesn't work. - ws, err := client.Workspace(setupCtx, r.Workspace.ID) - require.NoError(t, err) - require.NotEmpty(t, ws.LatestBuild.Resources) - require.NotEmpty(t, ws.LatestBuild.Resources[0].Agents) - agentID := ws.LatestBuild.Resources[0].Agents[0].ID - - // Given: the workspace agent has written logs. - agentClient.PatchLogs(setupCtx, agentsdk.PatchLogs{ - Logs: []agentsdk.Log{ - { - CreatedAt: time.Now(), - Level: codersdk.LogLevelInfo, - Output: "test log message", - }, - }, - }) - - t.Run("ReportTask", func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient, toolsdk.WithAgentClient(agentClient), toolsdk.WithAppStatusSlug("some-agent-app")) - require.NoError(t, err) - _, err = testTool(t, toolsdk.ReportTask, tb, toolsdk.ReportTaskArgs{ - Summary: "test summary", - State: "complete", - Link: "https://example.com", - }) - require.NoError(t, err) - }) - - t.Run("GetWorkspace", func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - result, err := testTool(t, toolsdk.GetWorkspace, tb, toolsdk.GetWorkspaceArgs{ - WorkspaceID: r.Workspace.ID.String(), - }) - - require.NoError(t, err) - require.Equal(t, r.Workspace.ID, result.ID, "expected the workspace ID to match") - }) - - t.Run("ListTemplates", func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - // Get the templates directly for comparison - expected, err := memberClient.Templates(context.Background(), codersdk.TemplateFilter{}) - require.NoError(t, err) - - result, err := testTool(t, toolsdk.ListTemplates, tb, toolsdk.NoArgs{}) - - require.NoError(t, err) - require.Len(t, result, len(expected)) - - // Sort the results by name to ensure the order is consistent - sort.Slice(expected, func(a, b int) bool { - return expected[a].Name < expected[b].Name - }) - sort.Slice(result, func(a, b int) bool { - return result[a].Name < result[b].Name - }) - for i, template := range result { - require.Equal(t, expected[i].ID.String(), template.ID) - } - }) - - t.Run("Whoami", func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - result, err := testTool(t, toolsdk.GetAuthenticatedUser, tb, toolsdk.NoArgs{}) - - require.NoError(t, err) - require.Equal(t, member.ID, result.ID) - require.Equal(t, member.Username, result.Username) - }) - - t.Run("ListWorkspaces", func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - result, err := testTool(t, toolsdk.ListWorkspaces, tb, toolsdk.ListWorkspacesArgs{}) - - require.NoError(t, err) - require.Len(t, result, 1, "expected 1 workspace") - workspace := result[0] - require.Equal(t, r.Workspace.ID.String(), workspace.ID, "expected the workspace to match the one we created") - }) - - t.Run("CreateWorkspaceBuild", func(t *testing.T) { - t.Run("Stop", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitShort) - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - result, err := testTool(t, toolsdk.CreateWorkspaceBuild, tb, toolsdk.CreateWorkspaceBuildArgs{ - WorkspaceID: r.Workspace.ID.String(), - Transition: "stop", - }) - - require.NoError(t, err) - require.Equal(t, codersdk.WorkspaceTransitionStop, result.Transition) - require.Equal(t, r.Workspace.ID, result.WorkspaceID) - require.Equal(t, r.TemplateVersion.ID, result.TemplateVersionID) - require.Equal(t, codersdk.WorkspaceTransitionStop, result.Transition) - - // Important: cancel the build. We don't run any provisioners, so this - // will remain in the 'pending' state indefinitely. - require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID)) - }) - - t.Run("Start", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitShort) - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - result, err := testTool(t, toolsdk.CreateWorkspaceBuild, tb, toolsdk.CreateWorkspaceBuildArgs{ - WorkspaceID: r.Workspace.ID.String(), - Transition: "start", - }) - - require.NoError(t, err) - require.Equal(t, codersdk.WorkspaceTransitionStart, result.Transition) - require.Equal(t, r.Workspace.ID, result.WorkspaceID) - require.Equal(t, r.TemplateVersion.ID, result.TemplateVersionID) - require.Equal(t, codersdk.WorkspaceTransitionStart, result.Transition) - - // Important: cancel the build. We don't run any provisioners, so this - // will remain in the 'pending' state indefinitely. - require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID)) - }) - - t.Run("TemplateVersionChange", func(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitShort) - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - // Get the current template version ID before updating - workspace, err := memberClient.Workspace(ctx, r.Workspace.ID) - require.NoError(t, err) - originalVersionID := workspace.LatestBuild.TemplateVersionID - - // Create a new template version to update to - newVersion := dbfake.TemplateVersion(t, store). - // nolint:gocritic // This is in a test package and does not end up in the build - Seed(database.TemplateVersion{ - OrganizationID: owner.OrganizationID, - CreatedBy: owner.UserID, - TemplateID: uuid.NullUUID{UUID: r.Template.ID, Valid: true}, - }).Do() - - // Update to new version - updateBuild, err := testTool(t, toolsdk.CreateWorkspaceBuild, tb, toolsdk.CreateWorkspaceBuildArgs{ - WorkspaceID: r.Workspace.ID.String(), - Transition: "start", - TemplateVersionID: newVersion.TemplateVersion.ID.String(), - }) - require.NoError(t, err) - require.Equal(t, codersdk.WorkspaceTransitionStart, updateBuild.Transition) - require.Equal(t, r.Workspace.ID.String(), updateBuild.WorkspaceID.String()) - require.Equal(t, newVersion.TemplateVersion.ID.String(), updateBuild.TemplateVersionID.String()) - // Cancel the build so it doesn't remain in the 'pending' state indefinitely. - require.NoError(t, client.CancelWorkspaceBuild(ctx, updateBuild.ID)) - - // Roll back to the original version - rollbackBuild, err := testTool(t, toolsdk.CreateWorkspaceBuild, tb, toolsdk.CreateWorkspaceBuildArgs{ - WorkspaceID: r.Workspace.ID.String(), - Transition: "start", - TemplateVersionID: originalVersionID.String(), - }) - require.NoError(t, err) - require.Equal(t, codersdk.WorkspaceTransitionStart, rollbackBuild.Transition) - require.Equal(t, r.Workspace.ID.String(), rollbackBuild.WorkspaceID.String()) - require.Equal(t, originalVersionID.String(), rollbackBuild.TemplateVersionID.String()) - // Cancel the build so it doesn't remain in the 'pending' state indefinitely. - require.NoError(t, client.CancelWorkspaceBuild(ctx, rollbackBuild.ID)) - }) - }) - - t.Run("ListTemplateVersionParameters", func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - params, err := testTool(t, toolsdk.ListTemplateVersionParameters, tb, toolsdk.ListTemplateVersionParametersArgs{ - TemplateVersionID: r.TemplateVersion.ID.String(), - }) - - require.NoError(t, err) - require.Empty(t, params) - }) - - t.Run("GetWorkspaceAgentLogs", func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - logs, err := testTool(t, toolsdk.GetWorkspaceAgentLogs, tb, toolsdk.GetWorkspaceAgentLogsArgs{ - WorkspaceAgentID: agentID.String(), - }) - - require.NoError(t, err) - require.NotEmpty(t, logs) - }) - - t.Run("GetWorkspaceBuildLogs", func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - logs, err := testTool(t, toolsdk.GetWorkspaceBuildLogs, tb, toolsdk.GetWorkspaceBuildLogsArgs{ - WorkspaceBuildID: r.Build.ID.String(), - }) - - require.NoError(t, err) - _ = logs // The build may not have any logs yet, so we just check that the function returns successfully - }) - - t.Run("GetTemplateVersionLogs", func(t *testing.T) { - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - logs, err := testTool(t, toolsdk.GetTemplateVersionLogs, tb, toolsdk.GetTemplateVersionLogsArgs{ - TemplateVersionID: r.TemplateVersion.ID.String(), - }) - - require.NoError(t, err) - _ = logs // Just ensuring the call succeeds - }) - - t.Run("UpdateTemplateActiveVersion", func(t *testing.T) { - tb, err := toolsdk.NewDeps(client) - require.NoError(t, err) - result, err := testTool(t, toolsdk.UpdateTemplateActiveVersion, tb, toolsdk.UpdateTemplateActiveVersionArgs{ - TemplateID: r.Template.ID.String(), - TemplateVersionID: r.TemplateVersion.ID.String(), - }) - - require.NoError(t, err) - require.Contains(t, result, "Successfully updated") - }) - - t.Run("DeleteTemplate", func(t *testing.T) { - tb, err := toolsdk.NewDeps(client) - require.NoError(t, err) - _, err = testTool(t, toolsdk.DeleteTemplate, tb, toolsdk.DeleteTemplateArgs{ - TemplateID: r.Template.ID.String(), - }) - - // This will fail with because there already exists a workspace. - require.ErrorContains(t, err, "All workspaces must be deleted before a template can be removed") - }) - - t.Run("UploadTarFile", func(t *testing.T) { - files := map[string]string{ - "main.tf": `resource "null_resource" "example" {}`, - } - tb, err := toolsdk.NewDeps(memberClient) - require.NoError(t, err) - - result, err := testTool(t, toolsdk.UploadTarFile, tb, toolsdk.UploadTarFileArgs{ - Files: files, - }) - - require.NoError(t, err) - require.NotEmpty(t, result.ID) - }) - - t.Run("CreateTemplateVersion", func(t *testing.T) { - tb, err := toolsdk.NewDeps(client) - require.NoError(t, err) - // nolint:gocritic // This is in a test package and does not end up in the build - file := dbgen.File(t, store, database.File{}) - t.Run("WithoutTemplateID", func(t *testing.T) { - tv, err := testTool(t, toolsdk.CreateTemplateVersion, tb, toolsdk.CreateTemplateVersionArgs{ - FileID: file.ID.String(), - }) - require.NoError(t, err) - require.NotEmpty(t, tv) - }) - t.Run("WithTemplateID", func(t *testing.T) { - tv, err := testTool(t, toolsdk.CreateTemplateVersion, tb, toolsdk.CreateTemplateVersionArgs{ - FileID: file.ID.String(), - TemplateID: r.Template.ID.String(), - }) - require.NoError(t, err) - require.NotEmpty(t, tv) - }) - }) - - t.Run("CreateTemplate", func(t *testing.T) { - tb, err := toolsdk.NewDeps(client) - require.NoError(t, err) - // Create a new template version for use here. - tv := dbfake.TemplateVersion(t, store). - // nolint:gocritic // This is in a test package and does not end up in the build - Seed(database.TemplateVersion{OrganizationID: owner.OrganizationID, CreatedBy: owner.UserID}). - SkipCreateTemplate().Do() - - // We're going to re-use the pre-existing template version - _, err = testTool(t, toolsdk.CreateTemplate, tb, toolsdk.CreateTemplateArgs{ - Name: testutil.GetRandomNameHyphenated(t), - DisplayName: "Test Template", - Description: "This is a test template", - VersionID: tv.TemplateVersion.ID.String(), - }) - - require.NoError(t, err) - }) - - t.Run("CreateWorkspace", func(t *testing.T) { - tb, err := toolsdk.NewDeps(client) - require.NoError(t, err) - // We need a template version ID to create a workspace - res, err := testTool(t, toolsdk.CreateWorkspace, tb, toolsdk.CreateWorkspaceArgs{ - User: "me", - TemplateVersionID: r.TemplateVersion.ID.String(), - Name: testutil.GetRandomNameHyphenated(t), - RichParameters: map[string]string{}, - }) - - // The creation might fail for various reasons, but the important thing is - // to mark it as tested - require.NoError(t, err) - require.NotEmpty(t, res.ID, "expected a workspace ID") - }) -} - -// TestedTools keeps track of which tools have been tested. -var testedTools sync.Map - -// testTool is a helper function to test a tool and mark it as tested. -// Note that we test the _generic_ version of the tool and not the typed one. -// This is to mimic how we expect external callers to use the tool. -func testTool[Arg, Ret any](t *testing.T, tool toolsdk.Tool[Arg, Ret], tb toolsdk.Deps, args Arg) (Ret, error) { - t.Helper() - defer func() { testedTools.Store(tool.Tool.Name, true) }() - toolArgs, err := json.Marshal(args) - require.NoError(t, err, "failed to marshal args") - result, err := tool.Generic().Handler(context.Background(), tb, toolArgs) - var ret Ret - require.NoError(t, json.Unmarshal(result, &ret), "failed to unmarshal result %q", string(result)) - return ret, err -} - -func TestWithRecovery(t *testing.T) { - t.Parallel() - t.Run("OK", func(t *testing.T) { - t.Parallel() - fakeTool := toolsdk.GenericTool{ - Tool: aisdk.Tool{ - Name: "echo", - Description: "Echoes the input.", - }, - Handler: func(ctx context.Context, tb toolsdk.Deps, args json.RawMessage) (json.RawMessage, error) { - return args, nil - }, - } - - wrapped := toolsdk.WithRecover(fakeTool.Handler) - v, err := wrapped(context.Background(), toolsdk.Deps{}, []byte(`{}`)) - require.NoError(t, err) - require.JSONEq(t, `{}`, string(v)) - }) - - t.Run("Error", func(t *testing.T) { - t.Parallel() - fakeTool := toolsdk.GenericTool{ - Tool: aisdk.Tool{ - Name: "fake_tool", - Description: "Returns an error for testing.", - }, - Handler: func(ctx context.Context, tb toolsdk.Deps, args json.RawMessage) (json.RawMessage, error) { - return nil, assert.AnError - }, - } - wrapped := toolsdk.WithRecover(fakeTool.Handler) - v, err := wrapped(context.Background(), toolsdk.Deps{}, []byte(`{}`)) - require.Nil(t, v) - require.ErrorIs(t, err, assert.AnError) - }) - - t.Run("Panic", func(t *testing.T) { - t.Parallel() - panicTool := toolsdk.GenericTool{ - Tool: aisdk.Tool{ - Name: "panic_tool", - Description: "Panics for testing.", - }, - Handler: func(ctx context.Context, tb toolsdk.Deps, args json.RawMessage) (json.RawMessage, error) { - panic("you can't sweat this fever out") - }, - } - - wrapped := toolsdk.WithRecover(panicTool.Handler) - v, err := wrapped(context.Background(), toolsdk.Deps{}, []byte("disco")) - require.Empty(t, v) - require.ErrorContains(t, err, "you can't sweat this fever out") - }) -} - -type testContextKey struct{} - -func TestWithCleanContext(t *testing.T) { - t.Parallel() - - t.Run("NoContextKeys", func(t *testing.T) { - t.Parallel() - - // This test is to ensure that the context values are not set in the - // toolsdk package. - ctxTool := toolsdk.GenericTool{ - Tool: aisdk.Tool{ - Name: "context_tool", - Description: "Returns the context value for testing.", - }, - Handler: func(toolCtx context.Context, tb toolsdk.Deps, args json.RawMessage) (json.RawMessage, error) { - v := toolCtx.Value(testContextKey{}) - assert.Nil(t, v, "expected the context value to be nil") - return nil, nil - }, - } - - wrapped := toolsdk.WithCleanContext(ctxTool.Handler) - ctx := context.WithValue(context.Background(), testContextKey{}, "test") - _, _ = wrapped(ctx, toolsdk.Deps{}, []byte(`{}`)) - }) - - t.Run("PropagateCancel", func(t *testing.T) { - t.Parallel() - - // This test is to ensure that the context is canceled properly. - callCh := make(chan struct{}) - ctxTool := toolsdk.GenericTool{ - Tool: aisdk.Tool{ - Name: "context_tool", - Description: "Returns the context value for testing.", - }, - Handler: func(toolCtx context.Context, tb toolsdk.Deps, args json.RawMessage) (json.RawMessage, error) { - defer close(callCh) - // Wait for the context to be canceled - <-toolCtx.Done() - return nil, toolCtx.Err() - }, - } - wrapped := toolsdk.WithCleanContext(ctxTool.Handler) - errCh := make(chan error, 1) - - tCtx := testutil.Context(t, testutil.WaitShort) - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - go func() { - _, err := wrapped(ctx, toolsdk.Deps{}, []byte(`{}`)) - errCh <- err - }() - - cancel() - - // Ensure the tool is called - select { - case <-callCh: - case <-tCtx.Done(): - require.Fail(t, "test timed out before handler was called") - } - - // Ensure the correct error is returned - select { - case <-tCtx.Done(): - require.Fail(t, "test timed out") - case err := <-errCh: - // Context was canceled and the done channel was closed - require.ErrorIs(t, err, context.Canceled) - } - }) - - t.Run("PropagateDeadline", func(t *testing.T) { - t.Parallel() - - // This test ensures that the context deadline is propagated to the child - // from the parent. - ctxTool := toolsdk.GenericTool{ - Tool: aisdk.Tool{ - Name: "context_tool_deadline", - Description: "Checks if context has deadline.", - }, - Handler: func(toolCtx context.Context, tb toolsdk.Deps, args json.RawMessage) (json.RawMessage, error) { - _, ok := toolCtx.Deadline() - assert.True(t, ok, "expected deadline to be set on the child context") - return nil, nil - }, - } - - wrapped := toolsdk.WithCleanContext(ctxTool.Handler) - parent, cancel := context.WithTimeout(context.Background(), testutil.IntervalFast) - t.Cleanup(cancel) - _, err := wrapped(parent, toolsdk.Deps{}, []byte(`{}`)) - require.NoError(t, err) - }) -} - -func TestToolSchemaFields(t *testing.T) { - t.Parallel() - - // Test that all tools have the required Schema fields (Properties and Required) - for _, tool := range toolsdk.All { - t.Run(tool.Tool.Name, func(t *testing.T) { - t.Parallel() - - // Check that Properties is not nil - require.NotNil(t, tool.Tool.Schema.Properties, - "Tool %q missing Schema.Properties", tool.Tool.Name) - - // Check that Required is not nil - require.NotNil(t, tool.Tool.Schema.Required, - "Tool %q missing Schema.Required", tool.Tool.Name) - - // Ensure Properties has entries for all required fields - for _, requiredField := range tool.Tool.Schema.Required { - _, exists := tool.Tool.Schema.Properties[requiredField] - require.True(t, exists, - "Tool %q requires field %q but it is not defined in Properties", - tool.Tool.Name, requiredField) - } - }) - } -} - -// TestMain runs after all tests to ensure that all tools in this package have -// been tested once. -func TestMain(m *testing.M) { - // Initialize testedTools - for _, tool := range toolsdk.All { - testedTools.Store(tool.Tool.Name, false) - } - - code := m.Run() - - // Ensure all tools have been tested - var untested []string - for _, tool := range toolsdk.All { - if tested, ok := testedTools.Load(tool.Tool.Name); !ok || !tested.(bool) { - untested = append(untested, tool.Tool.Name) - } - } - - if len(untested) > 0 && code == 0 { - code = 1 - println("The following tools were not tested:") - for _, tool := range untested { - println(" - " + tool) - } - println("Please ensure that all tools are tested using testTool().") - println("If you just added a new tool, please add a test for it.") - println("NOTE: if you just ran an individual test, this is expected.") - } - - // Check for goroutine leaks. Below is adapted from goleak.VerifyTestMain: - if code == 0 { - if err := goleak.Find(testutil.GoleakOptions...); err != nil { - println("goleak: Errors on successful test run: ", err.Error()) - code = 1 - } - } - - os.Exit(code) -} diff --git a/go.mod b/go.mod index da5fab466887a..d90a8d7ad3935 100644 --- a/go.mod +++ b/go.mod @@ -484,7 +484,7 @@ require ( ) require ( - github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 + github.com/anthropics/anthropic-sdk-go v1.4.0 github.com/charmbracelet/log v0.4.1 github.com/coder/freeway v0.0.0-20250514145842-67c0cca1dc72 github.com/coder/preview v0.0.2-0.20250527172548-ab173d35040c @@ -516,12 +516,12 @@ require ( github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/goccy/go-yaml v1.17.1 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/hashicorp/go-getter v1.7.8 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.4.0 // indirect + github.com/openai/openai-go v1.3.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/samber/lo v1.50.0 // indirect diff --git a/go.sum b/go.sum index b6f7be14b4213..fdddeb4326cda 100644 --- a/go.sum +++ b/go.sum @@ -717,8 +717,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 h1:b5t1ZJMvV/l99y4jbz7kRFdUp3BSDkI8EhSlHczivtw= -github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= +github.com/anthropics/anthropic-sdk-go v1.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0= +github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= @@ -1160,8 +1160,6 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= -github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= @@ -1632,6 +1630,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisti github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1/go.mod h1:Z/S1brD5gU2Ntht/bHxBVnGxXKTvZDr0dNv/riUzPmY= github.com/openai/openai-go v0.1.0-beta.10 h1:CknhGXe8aXQMRuqg255PFnWzgRY9nEryMxoNIBBM9tU= github.com/openai/openai-go v0.1.0-beta.10/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/openai/openai-go v1.3.0 h1:lBpvgXxGHUufk9DNTguval40y2oK0GHZwgWQyUtjPIQ= +github.com/openai/openai-go v1.3.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 9fa6e45fa30da..2fd2438a0c6cd 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -9,6 +9,8 @@ export interface ACLAvailable { // From codersdk/deployment.go export interface AIConfig { readonly providers?: readonly AIProviderConfig[]; + // embedded anonymous struct, please fix by naming it + readonly bridge?: unknown; } // From codersdk/deployment.go From dcf67662fb6c5eac05aa83734d4683b2988d5477 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 6 Jun 2025 18:06:17 +0200 Subject: [PATCH 07/61] chore: upgrade aisdk-go Signed-off-by: Danny Kopping --- coderd/ai/ai.go | 6 +- coderd/chat.go | 4 +- coderd/deployment.go | 2 +- codersdk/chat.go | 2 +- codersdk/toolsdk/toolsdk.go | 2 +- codersdk/toolsdk/toolsdk_test.go | 2 +- go.mod | 8 +- go.sum | 1546 +++++++++++++++++++++++++++++- site/src/api/typesGenerated.ts | 6 +- 9 files changed, 1554 insertions(+), 24 deletions(-) diff --git a/coderd/ai/ai.go b/coderd/ai/ai.go index 97c825ae44c06..d27e507dc9d3b 100644 --- a/coderd/ai/ai.go +++ b/coderd/ai/ai.go @@ -5,7 +5,7 @@ import ( "github.com/anthropics/anthropic-sdk-go" anthropicoption "github.com/anthropics/anthropic-sdk-go/option" - "github.com/kylecarbs/aisdk-go" + "github.com/coder/aisdk-go" "github.com/openai/openai-go" openaioption "github.com/openai/openai-go/option" "golang.org/x/xerrors" @@ -85,12 +85,12 @@ func ModelsFromConfig(ctx context.Context, configs []codersdk.AIProviderConfig) } if options.SystemPrompt != "" { systemMessage = []anthropic.TextBlockParam{ - *anthropic.NewTextBlock(options.SystemPrompt).OfRequestTextBlock, + *anthropic.NewTextBlock(options.SystemPrompt).OfText, } } return aisdk.AnthropicToDataStream(client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ Messages: anthropicMessages, - Model: options.Model, + Model: anthropic.Model(options.Model), System: systemMessage, Tools: aisdk.ToolsToAnthropic(options.Tools), MaxTokens: 8192, diff --git a/coderd/chat.go b/coderd/chat.go index b10211075cfe6..40b5ccee93a7e 100644 --- a/coderd/chat.go +++ b/coderd/chat.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/kylecarbs/aisdk-go" + "github.com/coder/aisdk-go" "github.com/coder/coder/v2/coderd/ai" "github.com/coder/coder/v2/coderd/database" @@ -301,7 +301,7 @@ You are running as an agent - please keep going until the user's query is comple }) return } - stream = stream.WithToolCalling(func(toolCall aisdk.ToolCall) aisdk.ToolCallResult { + stream = stream.WithToolCalling(func(toolCall aisdk.ToolCall) any { tool, ok := handlers[toolCall.Name] if !ok { return nil diff --git a/coderd/deployment.go b/coderd/deployment.go index 60988aeb2ce5a..90c07602714ab 100644 --- a/coderd/deployment.go +++ b/coderd/deployment.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "github.com/kylecarbs/aisdk-go" + "github.com/coder/aisdk-go" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/rbac" diff --git a/codersdk/chat.go b/codersdk/chat.go index 2093adaff95e8..e21bc3b6be26b 100644 --- a/codersdk/chat.go +++ b/codersdk/chat.go @@ -8,7 +8,7 @@ import ( "time" "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" + "github.com/coder/aisdk-go" "golang.org/x/xerrors" ) diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index a2a31cf431fc1..b283b92ff8bed 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -8,7 +8,7 @@ import ( "io" "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" + "github.com/coder/aisdk-go" "golang.org/x/xerrors" "github.com/coder/coder/v2/codersdk" diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index f9c35dba5951d..cb373db37bbb2 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" + "github.com/coder/aisdk-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" diff --git a/go.mod b/go.mod index 584b7f08cc373..9717c7708e57f 100644 --- a/go.mod +++ b/go.mod @@ -484,13 +484,13 @@ require ( ) require ( - github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 + github.com/anthropics/anthropic-sdk-go v1.4.0 + github.com/coder/aisdk-go v0.0.9 github.com/coder/preview v0.0.2-0.20250604144457-c9862a17f652 github.com/fsnotify/fsnotify v1.9.0 - github.com/kylecarbs/aisdk-go v0.0.8 github.com/mark3labs/mcp-go v0.31.0 - github.com/openai/openai-go v0.1.0-beta.10 - google.golang.org/genai v0.7.0 + github.com/openai/openai-go v1.3.0 + google.golang.org/genai v1.10.0 ) require ( diff --git a/go.sum b/go.sum index c48ca26edd6ce..7547317e11c21 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA= +4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= +4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= +4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb h1:4MKA8lBQLnCqj2myJCb5Lzoa65y0tABO4gHrxuMdsCQ= cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ= cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= @@ -43,42 +47,61 @@ cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit6 cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accessapproval v1.8.3 h1:axlU03FRiXDNupsmPG7LKzuS4Enk1gf598M62lWVB74= +cloud.google.com/go/accessapproval v1.8.3/go.mod h1:3speETyAv63TDrDmo5lIkpVueFkQcQchkiw/TAMbBo4= cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/accesscontextmanager v1.9.3 h1:8zVoeiBa4erMCLEXltOcqVEsZhS26JZ5/Vrgs59eQiI= +cloud.google.com/go/accesscontextmanager v1.9.3/go.mod h1:S1MEQV5YjkAKBoMekpGrkXKfrBdsi4x6Dybfq6gZ8BU= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/aiplatform v1.74.0 h1:rE2P5H7FOAFISAZilmdkapbk4CVgwfVs6FDWlhGfuy0= +cloud.google.com/go/aiplatform v1.74.0/go.mod h1:hVEw30CetNut5FrblYd1AJUWRVSIjoyIvp0EVUh51HA= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/analytics v0.26.0 h1:O2kWr2Sd4ep3I+YJ4aiY0G4+zWz6sp4eTce+JVns9TM= +cloud.google.com/go/analytics v0.26.0/go.mod h1:KZWJfs8uX/+lTjdIjvT58SFa86V9KM6aPXwZKK6uNVI= cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigateway v1.7.3 h1:Mn7cC5iWJz+cSMS/Hb+N2410CpZ6c8XpJKaexBl0Gxs= +cloud.google.com/go/apigateway v1.7.3/go.mod h1:uK0iRHdl2rdTe79bHW/bTsKhhXPcFihjUdb7RzhTPf4= cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeconnect v1.7.3 h1:Wlr+30Tha0SMCvQYZKdrh+HkpOyl0CQFSlzeY/Gg1gs= +cloud.google.com/go/apigeeconnect v1.7.3/go.mod h1:2ZkT5VCAqhYrDqf4dz7lGp4N/+LeNBSfou8Qs5bIuSg= cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apigeeregistry v0.9.3 h1:j9CJg/oC884OX5cDpiwNt1ZlDXNV6Zb9Mp1YmRrOG0k= +cloud.google.com/go/apigeeregistry v0.9.3/go.mod h1:oNCP2VjOeI6U8yuOuTmU4pkffdcXzR5KxeUD71gF+Dg= cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/appengine v1.9.3 h1:jrcanSzj9J1erevZuxldvsDwY+0k/DeFFzlnSfPGfL8= +cloud.google.com/go/appengine v1.9.3/go.mod h1:DtLsE/z3JufM/pCEIyVYebJ0h9UNPpN64GZQrYgOSyM= cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/area120 v0.9.3 h1:dPQ07rW4eku8OgNWDOaQaVGcE4+XfhH8BSbVwdVQ+wU= +cloud.google.com/go/area120 v0.9.3/go.mod h1:F3vxS/+hqzrjJo55Xvda3Jznjjbd+4Foo43SN5eMd8M= cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= @@ -87,6 +110,8 @@ cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1 cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/artifactregistry v1.16.1 h1:ZNXGB6+T7VmWdf6//VqxLdZ/sk0no8W0ujanHeJwDRw= +cloud.google.com/go/artifactregistry v1.16.1/go.mod h1:sPvFPZhfMavpiongKwfg93EOwJ18Tnj9DIwTU9xWUgs= cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= @@ -95,12 +120,16 @@ cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAt cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/asset v1.20.4 h1:6oNgjcs5KCPGBD71G0IccK6TfeFsEtBTyQ3Q+Dn09bs= +cloud.google.com/go/asset v1.20.4/go.mod h1:DP09pZ+SoFWUZyPZx26xVroHk+6+9umnQv+01yfJxbM= cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/assuredworkloads v1.12.3 h1:RU1WhF1zMggdXAZ+ezYTn4Eh/FdiX7sz8lLXGERn4Po= +cloud.google.com/go/assuredworkloads v1.12.3/go.mod h1:iGBkyMGdtlsxhCi4Ys5SeuvIrPTeI6HeuEJt7qJgJT8= cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -110,16 +139,24 @@ cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/I cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/automl v1.14.4 h1:vkD+hQ75SMINMgJBT/KDpFYvfQLzJbtIQZdw0AWq8Rs= +cloud.google.com/go/automl v1.14.4/go.mod h1:sVfsJ+g46y7QiQXpVs9nZ/h8ntdujHm5xhjHW32b3n4= cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/baremetalsolution v1.3.3 h1:OL+KT+wCumdDhG44aeqGAdkwdT8Wa4Lh+o4INM+CQjw= +cloud.google.com/go/baremetalsolution v1.3.3/go.mod h1:uF9g08RfmXTF6ZKbXxixy5cGMGFcG6137Z99XjxLOUI= cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/batch v1.12.0 h1:lXuTaELvU0P0ARbTFxxdpOC/dFnZZeGglSw06BtO//8= +cloud.google.com/go/batch v1.12.0/go.mod h1:CATSBh/JglNv+tEU/x21Z47zNatLQ/gpGnpyKOzbbcM= cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= +cloud.google.com/go/beyondcorp v1.1.3 h1:ezavJc0Gzh4N8zBskO/DnUVMWPa8lqH/tmQSyaknmCA= +cloud.google.com/go/beyondcorp v1.1.3/go.mod h1:3SlVKnlczNTSQFuH5SSyLuRd4KaBSc8FH/911TuF/Cc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -133,38 +170,56 @@ cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/Zur cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/bigquery v1.66.2 h1:EKOSqjtO7jPpJoEzDmRctGea3c2EOGoexy8VyY9dNro= +cloud.google.com/go/bigquery v1.66.2/go.mod h1:+Yd6dRyW8D/FYEjUGodIbu0QaoEmgav7Lwhotup6njo= +cloud.google.com/go/bigtable v1.35.0 h1:UEacPwaejN2mNbz67i1Iy3G812rxtgcs6ePj1TAg7dw= +cloud.google.com/go/bigtable v1.35.0/go.mod h1:EabtwwmTcOJFXp+oMZAT/jZkyDIjNwrv53TrS4DGrrM= cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/billing v1.20.1 h1:xMlO3hc5BI0s23tRB40bL40xSpxUR1x3E07Y5/VWcjU= +cloud.google.com/go/billing v1.20.1/go.mod h1:DhT80hUZ9gz5UqaxtK/LNoDELfxH73704VTce+JZqrY= cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/binaryauthorization v1.9.3 h1:X8JRfmk0/vyRqLusEyAPr0nZCK6RKae9omB4lrit0XI= +cloud.google.com/go/binaryauthorization v1.9.3/go.mod h1:f3xcb/7vWklDoF+q2EaAIS+/A/e1278IgiYxonRX+Jk= cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/certificatemanager v1.9.3 h1:2UP31fg7b+y3F0OmNbPHOKPEJ+6LOMfxAXX4p8xGCy4= +cloud.google.com/go/certificatemanager v1.9.3/go.mod h1:O5T4Lg/dHbDHLFFooV2Mh/VsT3Mj2CzPEWRo4qw5prc= cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/channel v1.19.2 h1:oHyO3QAZ6kdf6SwqnUTBz50ND6Nk2rxZtboUiF4dgLE= +cloud.google.com/go/channel v1.19.2/go.mod h1:syX5opXGXFt17DHCyCdbdlM464Tx0gHMi46UlEWY9Gg= cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/cloudbuild v1.22.0 h1:zmDznviZpvkCla0adbp7jJsMYZ9bABCbcPK2cBUHwg8= +cloud.google.com/go/cloudbuild v1.22.0/go.mod h1:p99MbQrzcENHb/MqU3R6rpqFRk/X+lNG3PdZEIhM95Y= cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/clouddms v1.8.4 h1:CDOd1nwmP4uek+nZhl4bhRIpzj8jMqoMRqKAfKlgLhw= +cloud.google.com/go/clouddms v1.8.4/go.mod h1:RadeJ3KozRwy4K/gAs7W74ZU3GmGgVq5K8sRqNs3HfA= cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/cloudtasks v1.13.3 h1:rXdznKjCa7WpzmvR2plrn2KJ+RZC1oYxPiRWNQjjf3k= +cloud.google.com/go/cloudtasks v1.13.3/go.mod h1:f9XRvmuFTm3VhIKzkzLCPyINSU3rjjvFUsFVGR5wi24= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= @@ -180,6 +235,8 @@ cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63 cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute v1.34.0 h1:+k/kmViu4TEi97NGaxAATYtpYBviOWJySPZ+ekA95kk= +cloud.google.com/go/compute v1.34.0/go.mod h1:zWZwtLwZQyonEvIQBuIa0WvraMYK69J5eDCOw9VZU4g= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= @@ -189,15 +246,21 @@ cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQ cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/contactcenterinsights v1.17.1 h1:xJoZbX0HM1zht8KxAB38hs2v4Hcl+vXGLo454LrdwxA= +cloud.google.com/go/contactcenterinsights v1.17.1/go.mod h1:n8OiNv7buLA2AkGVkfuvtW3HU13AdTmEwAlAu46bfxY= cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/container v1.42.2 h1:8ncSEBjkng6ucCICauaUGzBomoM2VyYzleAum1OFcow= +cloud.google.com/go/container v1.42.2/go.mod h1:y71YW7uR5Ck+9Vsbst0AF2F3UMgqmsN4SP8JR9xEsR8= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/containeranalysis v0.13.3 h1:1D8U75BeotZxrG4jR6NYBtOt+uAeBsWhpBZmSYLakQw= +cloud.google.com/go/containeranalysis v0.13.3/go.mod h1:0SYnagA1Ivb7qPqKNYPkCtphhkJn3IzgaSp3mj+9XAY= cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= @@ -206,44 +269,67 @@ cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOX cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/datacatalog v1.24.3 h1:3bAfstDB6rlHyK0TvqxEwaeOvoN9UgCs2bn03+VXmss= +cloud.google.com/go/datacatalog v1.24.3/go.mod h1:Z4g33XblDxWGHngDzcpfeOU0b1ERlDPTuQoYG6NkF1s= cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataflow v0.10.3 h1:+7IfIXzYWSybIIDGK9FN2uqBsP/5b/Y0pBYzNhcmKSU= +cloud.google.com/go/dataflow v0.10.3/go.mod h1:5EuVGDh5Tg4mDePWXMMGAG6QYAQhLNyzxdNQ0A1FfW4= cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/dataform v0.10.3 h1:ZpGkZV8OyhUhvN/tfLffU2ki5ERTtqOunkIaiVAhmw0= +cloud.google.com/go/dataform v0.10.3/go.mod h1:8SruzxHYCxtvG53gXqDZvZCx12BlsUchuV/JQFtyTCw= cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datafusion v1.8.3 h1:FTMtsf2nfGGlDCuE84/RvVaCcTIYE7WQSB0noeO0cwI= +cloud.google.com/go/datafusion v1.8.3/go.mod h1:hyglMzE57KRf0Rf/N2VRPcHCwKfZAAucx+LATY6Jc6Q= cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/datalabeling v0.9.3 h1:PqoA3gnOWaLcHCnqoZe4jh3jmiv6+Z7W2xUUkw/j4jE= +cloud.google.com/go/datalabeling v0.9.3/go.mod h1:3LDFUgOx+EuNUzDyjU7VElO8L+b5LeaZEFA/ZU1O1XU= cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataplex v1.22.0 h1:j4hD6opb+gq9CJNPFIlIggoW8Kjymg8Wmy2mdHmQoiw= +cloud.google.com/go/dataplex v1.22.0/go.mod h1:g166QMCGHvwc3qlTG4p34n+lHwu7JFfaNpMfI2uO7b8= cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataproc/v2 v2.11.0 h1:6aRpyoRfNOP+r2+pGb7HeHtF+SYQID8kzztfHuK0plk= +cloud.google.com/go/dataproc/v2 v2.11.0/go.mod h1:9vgGrn57ra7KBqz+B2KD+ltzEXvnHAUClFgq/ryU99g= cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +cloud.google.com/go/dataqna v0.9.3 h1:lGUj2FYs650EUPDMV6plWBAoh8qH9Bu1KCz1PUYF2VY= +cloud.google.com/go/dataqna v0.9.3/go.mod h1:PiAfkXxa2LZYxMnOWVYWz3KgY7txdFg9HEMQPb4u1JA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= +cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/datastream v1.13.0 h1:C5AeEdze55feJVb17a40QmlnyH/aMhn/uf3Go3hIqPA= +cloud.google.com/go/datastream v1.13.0/go.mod h1:GrL2+KC8mV4GjbVG43Syo5yyDXp3EH+t6N2HnZb1GOQ= cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/deploy v1.26.2 h1:1c2Cd3jdb0mrKHHfyzSQ5DRmxgYd07tIZZzuMNrwDxU= +cloud.google.com/go/deploy v1.26.2/go.mod h1:XpS3sG/ivkXCfzbzJXY9DXTeCJ5r68gIyeOgVGxGNEs= cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= @@ -252,35 +338,55 @@ cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFM cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dialogflow v1.66.0 h1:/kfpZw20/3v4sC8czEIuvn3Bu3qOne5aHDYlRYHbu18= +cloud.google.com/go/dialogflow v1.66.0/go.mod h1:BPiRTnnXP/tHLot5h/U62Xcp+i6ekRj/bq6uq88p+Lw= cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/dlp v1.21.0 h1:9kz7+gaB/0gBZsDUnNT1asDihNZSrRFSeUTBcBdUAkk= +cloud.google.com/go/dlp v1.21.0/go.mod h1:Y9HOVtPoArpL9sI1O33aN/vK9QRwDERU9PEJJfM8DvE= cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/documentai v1.35.2 h1:hswVobCWUTXtmn+4QqUIVkai7sDOe0QS2KB3IpqLkik= +cloud.google.com/go/documentai v1.35.2/go.mod h1:oh/0YXosgEq3hVhyH4ZQ7VNXPaveRO4eLVM3tBSZOsI= cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/domains v0.10.3 h1:wnqN5YwMrtLSjn+HB2sChgmZ6iocOta4Q41giQsiRjY= +cloud.google.com/go/domains v0.10.3/go.mod h1:m7sLe18p0PQab56bVH3JATYOJqyRHhmbye6gz7isC7o= cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/edgecontainer v1.4.1 h1:SwQuHQiheVfL7b5ar/AXDberiaqr/yiue8X55AdWnZU= +cloud.google.com/go/edgecontainer v1.4.1/go.mod h1:ubMQvXSxsvtEjJLyqcPFrdWrHfvjQxdoyt+SUrAi5ek= cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/errorreporting v0.3.2 h1:isaoPwWX8kbAOea4qahcmttoS79+gQhvKsfg5L5AgH8= +cloud.google.com/go/errorreporting v0.3.2/go.mod h1:s5kjs5r3l6A8UUyIsgvAhGq6tkqyBCUss0FRpsoVTww= cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/essentialcontacts v1.7.3 h1:Paw495vxVyKuAgcQ2NQk09iRZBhPYRytknydEnvzcv4= +cloud.google.com/go/essentialcontacts v1.7.3/go.mod h1:uimfZgDbhWNCmBpwUUPHe4vcMY2azsq/axC9f7vZFKI= cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/eventarc v1.15.1 h1:RMymT7R87LaxKugOKwooOoheWXUm1NMeOfh3CVU9g54= +cloud.google.com/go/eventarc v1.15.1/go.mod h1:K2luolBpwaVOujZQyx6wdG4n2Xum4t0q1cMBmY1xVyI= cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/filestore v1.9.3 h1:vTXQI5qYKZ8dmCyHN+zVfaMyXCYbyZNM0CkPzpPUn7Q= +cloud.google.com/go/filestore v1.9.3/go.mod h1:Me0ZRT5JngT/aZPIKpIK6N4JGMzrFHRtGHd9ayUS4R4= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= +cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= @@ -288,28 +394,42 @@ cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5Uwt cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/functions v1.19.3 h1:V0vCHSgFTUqKn57+PUXp1UfQY0/aMkveAw7wXeM3Lq0= +cloud.google.com/go/functions v1.19.3/go.mod h1:nOZ34tGWMmwfiSJjoH/16+Ko5106x+1Iji29wzrBeOo= cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc= cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkebackup v1.6.3 h1:djdExe/QgoKdp1gnIO1G5BoO1o/yGQOQJJEZ4QKTEXQ= +cloud.google.com/go/gkebackup v1.6.3/go.mod h1:JJzGsA8/suXpTDtqI7n9RZW97PXa2CIp+n8aRC/y57k= cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkeconnect v0.12.1 h1:YVpR0vlHSP/wD74PXEbKua4Aamud+wiYm4TiewNjD3M= +cloud.google.com/go/gkeconnect v0.12.1/go.mod h1:L1dhGY8LjINmWfR30vneozonQKRSIi5DWGIHjOqo58A= cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkehub v0.15.3 h1:yZ6lNJ9rNIoQmWrG14dB3+BFjS/EIRBf7Bo6jc5QWlE= +cloud.google.com/go/gkehub v0.15.3/go.mod h1:nzFT/Q+4HdQES/F+FP1QACEEWR9Hd+Sh00qgiH636cU= cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/gkemulticloud v1.5.1 h1:JWe6PDNpNU88ZYvQkTd7w28fgeIs/gg6i0hcjUkgZ3M= +cloud.google.com/go/gkemulticloud v1.5.1/go.mod h1:OdmhfSPXuJ0Kn9dQ2I3Ou7XZ3QK8caV4XVOJZwrIa3s= +cloud.google.com/go/grafeas v0.2.0 h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdELM= cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/gsuiteaddons v1.7.4 h1:f3eMYsCDdg2AeldIPdKmBRxN1WoiTpE3RvX5orcm/I8= +cloud.google.com/go/gsuiteaddons v1.7.4/go.mod h1:gpE2RUok+HUhuK7RPE/fCOEgnTffS0lCHRaAZLxAMeE= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= @@ -326,13 +446,19 @@ cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/ cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/iap v1.10.3 h1:OWNYFHPyIBNHEAEFdVKOltYWe0g3izSrpFJW6Iidovk= +cloud.google.com/go/iap v1.10.3/go.mod h1:xKgn7bocMuCFYhzRizRWP635E2LNPnIXT7DW0TlyPJ8= cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/ids v1.5.3 h1:wbFF7twu0XScFr+dtsVxTTttbFIRYt/SJjZiHFidtYE= +cloud.google.com/go/ids v1.5.3/go.mod h1:a2MX8g18Eqs7yxD/pnEdid42SyBUm9LIzSWf8Jux9OY= cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/iot v1.8.3 h1:aPWYQ+A1NX6ou/5U0nFAiXWdVT8OBxZYVZt2fBl2gWA= +cloud.google.com/go/iot v1.8.3/go.mod h1:dYhrZh+vUxIQ9m3uajyKRSW7moF/n0rYmA2PhYAkMFE= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= @@ -340,14 +466,20 @@ cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4 cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/kms v1.21.0 h1:x3EeWKuYwdlo2HLse/876ZrKjk2L5r7Uexfm8+p6mSI= +cloud.google.com/go/kms v1.21.0/go.mod h1:zoFXMhVVK7lQ3JC9xmhHMoQhnjEDZFoLAr5YMwzBLtk= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/language v1.14.3 h1:8hmFMiS3wjjj3TX/U1zZYTgzwZoUjDbo9PaqcYEmuB4= +cloud.google.com/go/language v1.14.3/go.mod h1:hjamj+KH//QzF561ZuU2J+82DdMlFUjmiGVWpovGGSA= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/lifesciences v0.10.3 h1:Z05C+Ui953f0EQx9hJ1la6+QQl8ADrIs3iNwP5Elkpg= +cloud.google.com/go/lifesciences v0.10.3/go.mod h1:hnUUFht+KcZcliixAg+iOh88FUwAzDQQt5tWd7iIpNg= cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= @@ -360,22 +492,32 @@ cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8 cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/managedidentities v1.7.3 h1:b9xGs24BIjfyvLgCtJoClOZpPi8d8owPgWe5JEINgaY= +cloud.google.com/go/managedidentities v1.7.3/go.mod h1:H9hO2aMkjlpY+CNnKWRh+WoQiUIDO8457wWzUGsdtLA= cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/maps v1.19.0 h1:deVm1ZFyCrUwxG11CdvtBz350VG5JUQ/LHTLnQrBgrM= +cloud.google.com/go/maps v1.19.0/go.mod h1:goHUXrmzoZvQjUVd0KGhH8t3AYRm17P8b+fsyR1UAmQ= cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/mediatranslation v0.9.3 h1:nRBjeaMLipw05Br+qDAlSCcCQAAlat4mvpafztbEVgc= +cloud.google.com/go/mediatranslation v0.9.3/go.mod h1:KTrFV0dh7duYKDjmuzjM++2Wn6yw/I5sjZQVV5k3BAA= cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/memcache v1.11.3 h1:XH/qT3GbbSH//R0JTqR77lRpBxaa0N9sHgAzfwbTrv0= +cloud.google.com/go/memcache v1.11.3/go.mod h1:UeWI9cmY7hvjU1EU6dwJcQb6EFG4GaM3KNXOO2OFsbI= cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/metastore v1.14.3 h1:jDqeCw6NGDRAPT9+2Y/EjnWAB0BfCcUfmPLOyhB0eHs= +cloud.google.com/go/metastore v1.14.3/go.mod h1:HlbGVOvg0ubBLVFRk3Otj3gtuzInuzO/TImOBwsKlG4= cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= @@ -388,49 +530,73 @@ cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uG cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkconnectivity v1.16.1 h1:YsVhG71ZC4FkqCP2oCI55x/JeGFyd7738Lt8iNTrzJw= +cloud.google.com/go/networkconnectivity v1.16.1/go.mod h1:GBC1iOLkblcnhcnfRV92j4KzqGBrEI6tT7LP52nZCTk= cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networkmanagement v1.18.0 h1:oEoFGPYxTBsY47h0zdoE2ojV5aU/541D83UmxfjHWaE= +cloud.google.com/go/networkmanagement v1.18.0/go.mod h1:yTxpAFuvQOOKgL3W7+k2Rp1bSKTxyRcZ5xNHGdHUM6w= cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/networksecurity v0.10.3 h1:JLJBFbxc8D7/OS81MyRoKhc2OvnVJxy5VMoQqqAhA7k= +cloud.google.com/go/networksecurity v0.10.3/go.mod h1:G85ABVcPscEgpw+gcu+HUxNZJWjn3yhTqEU7+SsltFM= cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/notebooks v1.12.3 h1:+9DrGJcZhCu6B2t0JJorekjIUBvg/KvBmXJYGmfvVvA= +cloud.google.com/go/notebooks v1.12.3/go.mod h1:I0pMxZct+8Rega2LYrXL8jGAGZgLchSmh8Ksc+0xNyA= cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/optimization v1.7.3 h1:JwQjjoBZJpsoMQe/3mhVBMVZuSdagHg2pGOnwh2Jk+E= +cloud.google.com/go/optimization v1.7.3/go.mod h1:GlYFp4Mju0ybK5FlOUtV6zvWC00TIScdbsPyF6Iv144= cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orchestration v1.11.4 h1:SFAsKyqvtS8VFcsq+JgXAeRkrksB9UH+AH7iFamkmlc= +cloud.google.com/go/orchestration v1.11.4/go.mod h1:UKR2JwogaZmDGnAcBgAQgCPn89QMqhXFUCYVhHd31vs= cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/orgpolicy v1.14.2 h1:WFvgmjq/FO5GiXlhebltA9N14KdbLMcgG88ME+SWeBo= +cloud.google.com/go/orgpolicy v1.14.2/go.mod h1:2fTDMT3X048iFKxc6DEgkG+a/gN+68qEgtPrHItKMzo= cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/osconfig v1.14.3 h1:cyf1PMK5c2/WOIr5r2lxjH/XBJMA9P4zC8Tm10i0z3M= +cloud.google.com/go/osconfig v1.14.3/go.mod h1:9D2MS1Etne18r/mAeW5jtto3toc9H1qu9wLNDG3NvQg= cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/oslogin v1.14.3 h1:yomxnFPk+ye0zd0mJ15nn9fH4Ns7ex4xA3ll+u2q59A= +cloud.google.com/go/oslogin v1.14.3/go.mod h1:fDEGODTG/W9ZGUTHTlMh8euXWC1fTcgjJ9Kcxxy14a8= cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/phishingprotection v0.9.3 h1:T5mGFV0ggBKg3qt9myFRiGJu+nIUucuHLAtVpAuQ08I= +cloud.google.com/go/phishingprotection v0.9.3/go.mod h1:ylzN9HruB/X7dD50I4sk+FfYzuPx9fm5JWsYI0t7ncc= cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/policytroubleshooter v1.11.3 h1:ekIWI8JbKkpOfrgH/THGamQE/D16tcVBYJyrkseVcYI= +cloud.google.com/go/policytroubleshooter v1.11.3/go.mod h1:AFHlORqh4AnMC0twc2yPKfzlozp3DO0yo9OfOd9aNOs= cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= +cloud.google.com/go/privatecatalog v0.10.4 h1:fu2LABMi7CgZORQ2oNGbc0hoZ0FTqLkjGqIgAV/Kc7U= +cloud.google.com/go/privatecatalog v0.10.4/go.mod h1:n/vXBT+Wq8B4nSRUJNDsmqla5BYjbVxOlHzS6PjiF+w= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -439,9 +605,14 @@ cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcd cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsub v1.47.0 h1:Ou2Qu4INnf7ykrFjGv2ntFOjVo8Nloh/+OffF4mUu9w= +cloud.google.com/go/pubsub v1.47.0/go.mod h1:LaENesmga+2u0nDtLkIOILskxsfvn/BXX9Ak1NFxOs8= cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/pubsublite v1.8.2 h1:jLQozsEVr+c6tOU13vDugtnaBSUy/PD5zK6mhm+uF1Y= +cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= +cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= @@ -450,46 +621,66 @@ cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recaptchaenterprise/v2 v2.19.4 h1:T5YGzaXwTesHaPDNTAuU3neDwZEnfjce70zufPFUwno= +cloud.google.com/go/recaptchaenterprise/v2 v2.19.4/go.mod h1:WaglfocMJGkqZVdXY/FVB7OhoVRONPS4uXqtNn6HfX0= cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommendationengine v0.9.3 h1:kBpcYPx4ys4lrDGKp4OhP2uy8h7UjlmLW/qoO5Xb2bY= +cloud.google.com/go/recommendationengine v0.9.3/go.mod h1:QRnX5aM7DCvtqtSs7I0zay5Zfq3fzxqnsPbZF7pa1G8= cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/recommender v1.13.3 h1:dVlOjxsbjuhlwu4MIcyPWe09qVcDqc419iOjdPl5RHk= +cloud.google.com/go/recommender v1.13.3/go.mod h1:6yAmcfqJRKglZrVuTHsieTFEm4ai9JtY3nQzmX4TC0Q= cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/redis v1.18.0 h1:xcu35SCyHSp+nKV6QNIklgkBKTH1qb0aLUXjl0mSR8I= +cloud.google.com/go/redis v1.18.0/go.mod h1:fJ8dEQJQ7DY+mJRMkSafxQCuc8nOyPUwo9tXJqjvNEY= cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcemanager v1.10.3 h1:SHOMw0kX0xWratC5Vb5VULBeWiGlPYAs82kiZqNtWpM= +cloud.google.com/go/resourcemanager v1.10.3/go.mod h1:JSQDy1JA3K7wtaFH23FBGld4dMtzqCoOpwY55XYR8gs= cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/resourcesettings v1.8.3 h1:13HOFU7v4cEvIHXSAQbinF4wp2Baybbq7q9FMctg1Ek= +cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/retail v1.19.2 h1:PT6CUlazIFIOLLJnV+bPBtiSH8iusKZ+FZRzZYFt2vk= +cloud.google.com/go/retail v1.19.2/go.mod h1:71tRFYAcR4MhrZ1YZzaJxr030LvaZiIcupH7bXfFBcY= cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/run v1.9.0 h1:9WeTqeEcriXqRViXMNwczjFJjixOSBlSlk/fW3lfKPg= +cloud.google.com/go/run v1.9.0/go.mod h1:Dh0+mizUbtBOpPEzeXMM22t8qYQpyWpfmUiWQ0+94DU= cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/scheduler v1.11.4 h1:ewVvigBnEnrr9Ih8CKnLVoB5IiULaWfYU5nEnnfVAto= +cloud.google.com/go/scheduler v1.11.4/go.mod h1:0ylvH3syJnRi8EDVo9ETHW/vzpITR/b+XNnoF+GPSz4= cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/secretmanager v1.14.5 h1:W++V0EL9iL6T2+ec24Dm++bIti0tI6Gx6sCosDBters= +cloud.google.com/go/secretmanager v1.14.5/go.mod h1:GXznZF3qqPZDGZQqETZwZqHw4R6KCaYVvcGiRBA+aqY= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= @@ -497,16 +688,21 @@ cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/security v1.18.3 h1:ya9gfY1ign6Yy25VMMMgZ9xy7D/TczDB0ElXcyWmEVE= +cloud.google.com/go/security v1.18.3/go.mod h1:NmlSnEe7vzenMRoTLehUwa/ZTZHDQE59IPRevHcpCe4= cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/securitycenter v1.36.0 h1:IdDiAa7gYtL7Gdx+wEaNHimudk3ZkEGNhdz9FuEuxWM= +cloud.google.com/go/securitycenter v1.36.0/go.mod h1:AErAQqIvrSrk8cpiItJG1+ATl7SD7vQ6lgTFy/Tcs4Q= cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= @@ -514,26 +710,36 @@ cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPj cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicedirectory v1.12.3 h1:oFkCp6ti7fc7hzeROmOPQuPBHFqwyhcsv3Yrma28+uc= +cloud.google.com/go/servicedirectory v1.12.3/go.mod h1:dwTKSCYRD6IZMrqoBCIvZek+aOYK/6+jBzOGw8ks5aY= cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/shell v1.8.3 h1:mjYgUsOtV3jl9xvDmcvlRRmA64deEPf52zOfuc68b/g= +cloud.google.com/go/shell v1.8.3/go.mod h1:OYcrgWF6JSp/uk76sNTtYFlMD0ho2+Cdzc7U3P/bF54= cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/spanner v1.76.1 h1:vYbVZuXfnFwvNcvH3lhI2PeUA+kHyqKmLC7mJWaC4Ok= +cloud.google.com/go/spanner v1.76.1/go.mod h1:YtwoE+zObKY7+ZeDCBtZ2ukM+1/iPaMfUM+KnTh/sx0= cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/speech v1.26.0 h1:qvURtJs7BQzQhbxWxwai0pT79S8KLVKJ/4W8igVkt1Y= +cloud.google.com/go/speech v1.26.0/go.mod h1:78bqDV2SgwFlP/M4n3i3PwLthFq6ta7qmyG6lUV7UCA= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -550,17 +756,25 @@ cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POF cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/storagetransfer v1.12.1 h1:W3v9A7MGBN7H9sAFstyciwP/1XEQhUhZfrjclmDnpMs= +cloud.google.com/go/storagetransfer v1.12.1/go.mod h1:hQqbfs8/LTmObJyCC0KrlBw8yBJ2bSFlaGila0qBMk4= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/talent v1.8.0 h1:olv+s2g+LGXeJi+MYF1wI44/TwHaVnO0N7PiucVf5ZQ= +cloud.google.com/go/talent v1.8.0/go.mod h1:/gvOzSrtMcfTL/9xWhdYaZATaxUNhQ+L+3ZaGOGs7bA= cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/texttospeech v1.11.0 h1:YF/RdNb+jUEp22cIZCvqiFjfA5OxGE+Dxss3mhXU7oQ= +cloud.google.com/go/texttospeech v1.11.0/go.mod h1:7M2ro3I2QfIEvArFk1TJ+pqXJqhszDtxUpnIv/150As= cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/tpu v1.8.0 h1:BvMNijOb6Vd46Rr/SR5jWv1MPosOhVsi0UaeAGNjeds= +cloud.google.com/go/tpu v1.8.0/go.mod h1:XyNzyK1xc55WvL5rZEML0Z9/TUHDfnq0uICkQw6rWMo= cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= @@ -572,17 +786,24 @@ cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fp cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.12.3 h1:XJ7LipYJi80BCgVk2lx1fwc7DIYM6oV2qx1G4IAGQ5w= +cloud.google.com/go/translate v1.12.3/go.mod h1:qINOVpgmgBnY4YTFHdfVO4nLrSBlpvlIyosqpGEgyEg= cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.23.3 h1:C2FH+6yr6LCZC4fP0gm9FwJB/SRh5Ul88O5Sc/bL83I= +cloud.google.com/go/video v1.23.3/go.mod h1:Kvh/BheubZxGZDXSb0iO6YX7ZNcaYHbLjnnaC8Qyy3g= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/videointelligence v1.12.3 h1:zNTOUQyatGQtnCJ2dR3faRtpWQOlC8wszJqwG5CtwVM= +cloud.google.com/go/videointelligence v1.12.3/go.mod h1:dUA6V+NH7CVgX6TePq0IelVeBMGzvehxKPR4FGf1dtw= +cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= @@ -590,50 +811,120 @@ cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vision/v2 v2.9.3 h1:dPvfDuPqPH+Yscf0f2f1RprvKkoo+N/j0a+IbLYX7Cs= +cloud.google.com/go/vision/v2 v2.9.3/go.mod h1:weAcT8aNYSgrWWVTC2PuJTc7fcXKvUeAyDq8B6HkLSg= cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmmigration v1.8.3 h1:dpCQq3pj2HnKdbvGTftdWymm3r4ovF7JW5z8xBcO2x4= +cloud.google.com/go/vmmigration v1.8.3/go.mod h1:8CzUpK9eBzohgpL4RvBVtW4sY/sDliVyQonTFQfWcJ4= cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vmwareengine v1.3.3 h1:TfuQr5j7qriINulUMotaC/+27SQaW2thIkF3Gb6VJ38= +cloud.google.com/go/vmwareengine v1.3.3/go.mod h1:G7vz05KGijha0c0dj1INRKyDAaQW8TRMZt/FrfOZVXc= cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/vpcaccess v1.8.3 h1:vxVaoFM64M/ht619c4wZNF0iq0QPaMWElOh7Ns4r41A= +cloud.google.com/go/vpcaccess v1.8.3/go.mod h1:bqOhyeSh/nEmLIsIUoCiQCBHeNPNjaK9M3bIvKxFdsY= cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/webrisk v1.10.3 h1:yh0v/5n49VO4/i9pYfDm1gLJUj1Ph3Xzegn8WvK9YRA= +cloud.google.com/go/webrisk v1.10.3/go.mod h1:rRAqCA5/EQOX8ZEEF4HMIrLHGTK/Y1hEQgWMnih+jAw= cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/websecurityscanner v1.7.3 h1:/uxhVCWKXzPw5pVfnBOVjaSiQ6Bm0tDExDOCLV40thw= +cloud.google.com/go/websecurityscanner v1.7.3/go.mod h1:gy0Kmct4GNLoCePWs9xkQym1D7D59ld5AjhXrjipxSs= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +cloud.google.com/go/workflows v1.13.3 h1:lNFDMranJymDEB7cTI7DI9czbc1WU0RWY9KCEv9zuDY= +cloud.google.com/go/workflows v1.13.3/go.mod h1:Xi7wggEt/ljoEcyk+CB/Oa1AHBCk0T1f5UH/exBB5CE= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= +gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/gqlgen v0.17.36 h1:u/o/rv2SZ9s5280dyUOOrkpIIkr/7kITMXYD3rkJ9go= +github.com/99designs/gqlgen v0.17.36/go.mod h1:6RdyY8puhCoWAQVr2qzF2OMVfudQzc8ACxzpzluoQm4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/Abirdcfly/dupword v0.0.11 h1:z6v8rMETchZXUIuHxYNmlUAuKuB21PeaSymTed16wgU= +github.com/Abirdcfly/dupword v0.0.11/go.mod h1:wH8mVGuf3CP5fsBTkfWwwwKTjDnVVCxtU8d8rgeVYXA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Antonboom/errname v0.1.9 h1:BZDX4r3l4TBZxZ2o2LNrlGxSHran4d1u4veZdoORTT4= +github.com/Antonboom/errname v0.1.9/go.mod h1:nLTcJzevREuAsgTbG85UsuiWpMpAqbKD1HNZ29OzE58= +github.com/Antonboom/nilnil v0.1.4 h1:yWIfwbCRDpJiJvs7Quz55dzeXCgORQyAG29N9/J5H2Q= +github.com/Antonboom/nilnil v0.1.4/go.mod h1:iOov/7gRcXkeEU+EMGpBu2ORih3iyVEiWjeste1SJm8= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 h1:j8BorDEigD8UFOSZQiSqAMOOleyQOOQPnUAwV+Ls1gA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2/go.mod h1:dmXQgZuiSubAecswZE+Sm8jkvEa7kQgTPVRvwL/nd0E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= +github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= +github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= +github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= +github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= +github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/appsec-internal-go v1.9.0 h1:cGOneFsg0JTRzWl5U2+og5dbtyW3N8XaYwc5nXe39Vw= github.com/DataDog/appsec-internal-go v1.9.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g= github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1 h1:XHITEDEb6NVc9n+myS8KJhdK0vKOvY0BTWSFrFynm4s= github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1/go.mod h1:lzCtnMSGZm/3RMk5RBRW/6IuK1TNbDXx1ttHTxN5Ykc= +github.com/DataDog/datadog-agent/comp/trace/compression/def v0.64.0-rc.1 h1:yjO0qOUDDjC6i+w7eKGpXkkgPkpGP6D3ln1clFav62s= +github.com/DataDog/datadog-agent/comp/trace/compression/def v0.64.0-rc.1/go.mod h1:+BnlaBR5kMqTFYwDEc1QW0f6zvLJKnwBAUbeC68HaVs= +github.com/DataDog/datadog-agent/comp/trace/compression/impl-gzip v0.64.0-rc.1 h1:Rx2o4Sk1IwkpZbZu5PoLaai8rQjegwJ/eieEjryaU10= +github.com/DataDog/datadog-agent/comp/trace/compression/impl-gzip v0.64.0-rc.1/go.mod h1:mW8ei9lGT6E3uDsBLChNYIZ/GOUZNEwZh3Z+RaS/df4= +github.com/DataDog/datadog-agent/comp/trace/compression/impl-zstd v0.64.0-rc.1 h1:ADqb0FHp5TsTwk31aWy8hOp0XmizOsEKG6ByEIMfXYY= +github.com/DataDog/datadog-agent/comp/trace/compression/impl-zstd v0.64.0-rc.1/go.mod h1:qtNBiygMkl/zGi5v+ejj65hRoCQLRJadybMCFidfDKQ= github.com/DataDog/datadog-agent/pkg/obfuscate v0.64.0-rc.1 h1:63L66uiNazsZs1DCmb5aDv/YAkCqn6xKqc0aYeATkQ8= github.com/DataDog/datadog-agent/pkg/obfuscate v0.64.0-rc.1/go.mod h1:3BS4G7V1y7jhSgrbqPx2lGxBb/YomYwUP0wjwr+cBHc= github.com/DataDog/datadog-agent/pkg/proto v0.64.0-rc.1 h1:8+4sv0i+na4QMjggZrQNFspbVHu7iaZU6VWeupPMdbA= @@ -642,8 +933,12 @@ github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1 h1:MpUmwDTz github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1/go.mod h1:QHiOw0sFriX2whwein+Puv69CqJcbOQnocUBo2IahNk= github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1 h1:5PbiZw511B+qESc7PxxWY5ubiBtVnLFqC+UZKZAB3xo= github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1/go.mod h1:AkapH6q9UZLoRQuhlOPiibRFqZtaKPMwtzZwYjjzgK0= +github.com/DataDog/datadog-agent/pkg/util/cgroups v0.64.0-rc.1 h1:GqHjzbYflWAKQLv/yoNeC54lsrcZgxbAlX2nzfJzx8M= +github.com/DataDog/datadog-agent/pkg/util/cgroups v0.64.0-rc.1/go.mod h1:xQ8Es4XKlrWKUYp/ZY67vXRAjOLmfrGqMlNZ471y/vM= github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1 h1:5UHDao4MdRwRsf4ZEvMSbgoujHY/2Aj+TQ768ZrPXq8= github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1/go.mod h1:ZEm+kWbgm3alAsoVbYFM10a+PIxEW5KoVhV3kwiCuxE= +github.com/DataDog/datadog-agent/pkg/util/pointer v0.64.0-rc.1 h1:Qiiq60ysVOcEO3cO/2ffLyy1ZEZHU2ZybuJBACVvBSI= +github.com/DataDog/datadog-agent/pkg/util/pointer v0.64.0-rc.1/go.mod h1:cY4100zn21kBb748mG0hDv9BfswD+tUw3p2M4XdUzAE= github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1 h1:yqzXiCXrBXsQrbsFCTele7SgM6nK0bElDmBM0lsueIE= github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1/go.mod h1:9ZfE6J8Ty8xkgRuoH1ip9kvtlq6UaHwPOqxe9NJbVUE= github.com/DataDog/datadog-agent/pkg/version v0.64.0-rc.1 h1:eg+XW2CzOwFa//bjoXiw4xhNWWSdEJbMSC4TFcx6lVk= @@ -664,6 +959,14 @@ github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0 h1:GlvoS github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0/go.mod h1:mYQmU7mbHH6DrCaS8N6GZcxwPoeNfyuopUoLQltwSzs= github.com/DataDog/sketches-go v1.4.7 h1:eHs5/0i2Sdf20Zkj0udVFWuCrXGRFig2Dcfm5rtcTxc= github.com/DataDog/sketches-go v1.4.7/go.mod h1:eAmQ/EBmtSO+nQp7IZMZVRPT4BQTmIc5RZQ+deGlTPM= +github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= +github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Djarvur/go-err113 v0.1.0 h1:uCRZZOdMQ0TZPHYTdYpoC0bLYJKPEHPUJ8MeAa51lNU= +github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 h1:+r1rSv4gvYn0wmRjC8X7IAzX8QezqtFV9m0MUHFJgts= +github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0/go.mod h1:b3g59n2Y+T5xmcxJL+UEG2f8cQploZm1mR/v6BW0mU0= +github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible h1:juIaKLLVhqzP55d8x4cSVgwyQv76Z55/fRv/UBr2KkQ= +github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible/go.mod h1:BB1eHdMLYEFuFdBlRMb0N7YGVdM5s6Pt0njxgvfbGGs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= @@ -672,33 +975,74 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 h1:ig/FpDD2JofP/NExKQUbn7uOSZzJAQqogfqluZK4ed4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/IBM/sarama v1.40.0 h1:QTVmX+gMKye52mT5x+Ve/Bod2D0Gy7ylE2Wslv+RHtc= +github.com/IBM/sarama v1.40.0/go.mod h1:6pBloAs1WanL/vsq5qFTyTGulJUntZHhMLOUYEIs9mg= +github.com/Intevation/gval v1.3.0 h1:+Ze5sft5MmGbZrHj06NVUbcxCb67l9RaPTLMNr37mjw= +github.com/Intevation/gval v1.3.0/go.mod h1:xmGyGpP5be12EL0P12h+dqiYG8qn2j3PJxIgkoOHO5o= +github.com/Intevation/jsonpath v0.2.1 h1:rINNQJ0Pts5XTFEG+zamtdL7l9uuE1z0FBA+r55Sw+A= +github.com/Intevation/jsonpath v0.2.1/go.mod h1:WnZ8weMmwAx/fAO3SutjYFU+v7DFreNYnibV7CiaYIw= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= +github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/OpenPeeDeeP/depguard v1.1.1 h1:TSUznLjvp/4IUP+OQ0t/4jF4QUyxIcVX8YnghZdunyA= +github.com/OpenPeeDeeP/depguard v1.1.1/go.mod h1:JtAMzWkmFEzDPyAd+W0NHl1lvpQKTvT9jnRVsohBKpc= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= +github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= +github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= @@ -706,43 +1050,102 @@ github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= +github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= +github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= +github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/ammario/tlru v0.4.0 h1:sJ80I0swN3KOX2YxC6w8FbCqpQucWdbb+J36C05FPuU= github.com/ammario/tlru v0.4.0/go.mod h1:aYzRFu0XLo4KavE9W8Lx7tzjkX+pAApz+NgcKYIFUBQ= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 h1:b5t1ZJMvV/l99y4jbz7kRFdUp3BSDkI8EhSlHczivtw= -github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= +github.com/anthropics/anthropic-sdk-go v1.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0= +github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= +github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30xLN2sUZcMXl50hg+PJCIDdJgIvIbVcKqLJ/ZrtM= +github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8= +github.com/aquasecurity/defsec v0.94.1 h1:lk44bfUltm0f0Dw4DbO3Ka9d/bf3N8cWclSdHXMyKF4= +github.com/aquasecurity/defsec v0.94.1/go.mod h1:wiX9BX0SOG0ZWjVIPYGPl46fyO3Gu8lJnk4rmhFR7IA= +github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM= +github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s= +github.com/aquasecurity/go-npm-version v0.0.1 h1:2i/MM+A4KI8AJrqJa/Cwsa4qyljA8S/qngPyQiIVHcA= +github.com/aquasecurity/go-npm-version v0.0.1/go.mod h1:hxbJZtKlO4P8sZ9nztizR6XLoE33O+BkPmuYQ4ACyz0= +github.com/aquasecurity/go-pep440-version v0.0.1 h1:8VKKQtH2aV61+0hovZS3T//rUF+6GDn18paFTVS0h0M= +github.com/aquasecurity/go-pep440-version v0.0.1/go.mod h1:3naPe+Bp6wi3n4l5iBFCZgS0JG8vY6FT0H4NGhFJ+i4= github.com/aquasecurity/go-version v0.0.1 h1:4cNl516agK0TCn5F7mmYN+xVs1E3S45LkgZk3cbaW2E= github.com/aquasecurity/go-version v0.0.1/go.mod h1:s1UU6/v2hctXcOa3OLwfj5d9yoXHa3ahf+ipSwEvGT0= github.com/aquasecurity/iamgo v0.0.10 h1:t/HG/MI1eSephztDc+Rzh/YfgEa+NqgYRSfr6pHdSCQ= github.com/aquasecurity/iamgo v0.0.10/go.mod h1:GI9IQJL2a+C+V2+i3vcwnNKuIJXZ+HAfqxZytwy+cPk= github.com/aquasecurity/jfather v0.0.8 h1:tUjPoLGdlkJU0qE7dSzd1MHk2nQFNPR0ZfF+6shaExE= github.com/aquasecurity/jfather v0.0.8/go.mod h1:Ag+L/KuR/f8vn8okUi8Wc1d7u8yOpi2QTaGX10h71oY= +github.com/aquasecurity/table v1.10.0 h1:gPWV28qp9XSlvXdT3ku8yKQoZE6II0vsmegKpW+dB08= +github.com/aquasecurity/table v1.10.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw= +github.com/aquasecurity/testdocker v0.0.0-20240730042311-4642e94c7fc8 h1:b43UVqYjz7qDqK+cVOtF2Lk6CxjytYItP6Pgf3wGsNE= +github.com/aquasecurity/testdocker v0.0.0-20240730042311-4642e94c7fc8/go.mod h1:wXA9k3uuaxY3yu7gxrxZDPo/04FEMJtwyecdAlYrEIo= +github.com/aquasecurity/tml v0.6.1 h1:y2ZlGSfrhnn7t4ZJ/0rotuH+v5Jgv6BDDO5jB6A9gwo= +github.com/aquasecurity/tml v0.6.1/go.mod h1:OnYMWY5lvI9ejU7yH9LCberWaaTBW7hBFsITiIMY2yY= +github.com/aquasecurity/trivy-checks v1.10.0 h1:Q0FWsYy/uwvr/icRSOzNu55yDZ1ME8hZlpglNs62ZfE= +github.com/aquasecurity/trivy-checks v1.10.0/go.mod h1:/b633SOFNp8RjkxSq+FOg4SgxjklUp+BIQEyTWCnN1k= +github.com/aquasecurity/trivy-db v0.0.0-20250227071930-8bd8a9b89e2d h1:T16WrTi21YsMLQVhtp1r1hOIYK3x4BjnftpL9cp64Eo= +github.com/aquasecurity/trivy-db v0.0.0-20250227071930-8bd8a9b89e2d/go.mod h1:4bTsQPtMBN8v+UfUlE1aQBN1imftefnDafHBF85+aT8= github.com/aquasecurity/trivy-iac v0.8.0 h1:NKFhk/BTwQ0jIh4t74V8+6UIGUvPlaxO9HPlSMQi3fo= github.com/aquasecurity/trivy-iac v0.8.0/go.mod h1:ARiMeNqcaVWOXJmp8hmtMnNm/Jd836IOmDBUW5r4KEk= +github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48 h1:JVgBIuIYbwG+ekC5lUHUpGJboPYiCcxiz06RCtz8neI= +github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48/go.mod h1:Ldya37FLi0e/5Cjq2T5Bty7cFkzUDwTcPeQua+2M8i8= +github.com/aquasecurity/trivy-kubernetes v0.8.2 h1:8QP8fbUeC5y0kETDpk04XK52hYF6PsJOkd6HrbVnJqo= +github.com/aquasecurity/trivy-kubernetes v0.8.2/go.mod h1:O2SMbA3pnShkOCxBFT04ANYyDEvACbmd5t/XMH4uAFM= +github.com/aquasecurity/trivy-policies v0.8.0 h1:LvmIdw/DfTF72Lc8L+CKLYzfb5BFYzLBGFFR95PKC74= +github.com/aquasecurity/trivy-policies v0.8.0/go.mod h1:qF/t59pgK/0JTV6tXaeA3Iw3opzoMgzGCDcTDBmqb30= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/ashanbrown/forbidigo v1.5.1 h1:WXhzLjOlnuDYPYQo/eFlcFMi8X/kLfvWLYu6CSoebis= +github.com/ashanbrown/forbidigo v1.5.1/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= +github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= +github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= github.com/aslilac/afero v0.0.0-20250403163713-f06e86036696 h1:7hAl/81gNUjmSCqJYKe1aTIVY4myjapaSALdCko19tI= github.com/aslilac/afero v0.0.0-20250403163713-f06e86036696/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -754,6 +1157,8 @@ github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= @@ -762,16 +1167,48 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mln github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.1 h1:yg6nrV33ljY6CppoRnnsKLqIZ5ExNdQOGRBGNfc56Yw= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.1/go.mod h1:hGdIV5nndhIclFFvI1apVfQWn9ZKqedykZ1CtLZd03E= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.10 h1:fdLh7eMf5mxtggx2nG0+cFkaiRK+ULCOPK3qq8eTje4= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.10/go.mod h1:uBca+/1aH5v/RYWXqyymLrsbmx1vU9bBxeurlC627Gc= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.4 h1:x3V1JRHq7q9RUbDpaeNpLH7QoipGpCo3fdnMMuSeABU= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.4/go.mod h1:aryF4jxgjhbqpdhj8QybUZI3xYrX8MQIKm4WbOv8Whg= +github.com/aws/aws-sdk-go-v2/service/ebs v1.22.1 h1:SeDJWG4pmye+/aO6k+zt9clPTUy1MXqUmkW8rbAddQg= +github.com/aws/aws-sdk-go-v2/service/ebs v1.22.1/go.mod h1:wRzaW0v9GGQS0h//wpsVDw3Hah5gs5UP+NxoyGeZIGM= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.218.0 h1:QPYsTfcPpPhkF+37pxLcl3xbQz2SRxsShQNB6VCkvLo= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.218.0/go.mod h1:ouvGEfHbLaIlWwpDpOVWPWR+YwO0HDv3vm5tYLq8ImY= +github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 h1:E+UTVTDH6XTSjqxHWRuY8nB6s+05UllneWxnycplHFk= +github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU= +github.com/aws/aws-sdk-go-v2/service/eventbridge v1.20.4 h1:G18wotYZxZ0A5tkqKv6FHCjsF86UQrqNHy5LS+T7JWM= +github.com/aws/aws-sdk-go-v2/service/eventbridge v1.20.4/go.mod h1:XlbY5AGZhlipCdhRorT18/HEThKAxo51hMmhixreJoM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.34 h1:JlxVMFDHivlhNOIxd2O/9z4O0wC2zIC4lRB71lejVHU= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.34/go.mod h1:CDPcT6pljRaqz1yLsOgPUvOPOczFvXuJxOKzDzAbF0c= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.18.4 h1:UohaQds+Puk9BEbvncXkZduIGYImxohbFpVmSoymXck= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.18.4/go.mod h1:HnjgmL8TNmYtGcrA3N6EeCnDvlX6CteCdUbZ1wV8QWQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 h1:BRXS0U76Z8wfF+bnkilA2QwpIch6URlm++yPUt9QPmQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3/go.mod h1:bNXKFFyaiVvWuR6O16h/I1724+aXe/tAkA9/QS01t5k= +github.com/aws/aws-sdk-go-v2/service/sfn v1.19.4 h1:yIyFY2kbCOoHvuivf9minqnP2RLYJgmvQRYxakIb2oI= +github.com/aws/aws-sdk-go-v2/service/sfn v1.19.4/go.mod h1:uWCH4ATwNrkRO40j8Dmy7u/Y1/BVWgCM+YjBNYZeOro= +github.com/aws/aws-sdk-go-v2/service/sns v1.21.4 h1:Asj098jPfIZYzAbk4xVFwVBGij5hgMcli0d+5Pe4aZA= +github.com/aws/aws-sdk-go-v2/service/sns v1.21.4/go.mod h1:bbB779DXXOnPXvB7F3dP7AjuV1Eyr7fNyrA058ExuzY= +github.com/aws/aws-sdk-go-v2/service/sqs v1.24.4 h1:bp8KUUx15mnLMe8SSJqO/kYEn0C2kKfWq/M9SRK9i1E= +github.com/aws/aws-sdk-go-v2/service/sqs v1.24.4/go.mod h1:c1AF/ac4k4xz32FprEk6AqqGFH/Fkub9VUPSrASlllA= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 h1:hgSBvRT7JEWx2+vEGI9/Ld5rZtl7M5lu8PqdvOmbRHw= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4/go.mod h1:v7NIzEFIHBiicOMaMTuEmbnzGnqW0d+6ulNALul6fYE= github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= @@ -788,6 +1225,10 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bazelbuild/rules_go v0.44.2 h1:H2nzlC9VLKeVW1D90bahFSszpDE5qvtKr95Nz7BN0WQ= +github.com/bazelbuild/rules_go v0.44.2/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= +github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw= +github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= 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/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= @@ -806,47 +1247,99 @@ github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5k github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg= github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY= github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI= +github.com/bep/helpers v0.5.0 h1:rneezhnG7GzLFlsEWO/EnleaBRuluBDGFimalO6Y50o= +github.com/bep/helpers v0.5.0/go.mod h1:dSqCzIvHbzsk5YOesp1M7sKAq5xUcvANsRoKdawxH4Q= github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k= github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8= github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8= github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk= github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= +github.com/bep/mclib v1.20400.20402 h1:olpCE2WSPpOAbFE1R4hnftSEmQ34+xzy2HRzd0m69rA= +github.com/bep/mclib v1.20400.20402/go.mod h1:pkrk9Kyfqg34Uj6XlDq9tdEFJBiL1FvCoCgVKRzw1EY= github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE= github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro= +github.com/bep/simplecobra v0.6.0 h1:PpY/0PvYp6jt4OC/9SGoNPi6HzvpYzu8IPogVV6Xk90= +github.com/bep/simplecobra v0.6.0/go.mod h1:q0ecBAefJZYpzgkbPbQ901hzA98g3ZvCZWZRhzNtB5o= github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c h1:C4UZIaS+HAw+X6jGUsoP2ZbM99PuqhCttjomg1yhNAI= +github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo= +github.com/bkielbasa/cyclop v1.2.0 h1:7Jmnh0yL2DjKfw28p86YTd/B4lRGcNuu12sKE35sM7A= +github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= +github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= +github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= +github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bobuhiro11/gokvm v0.0.8-0.20231003020000-f53faca69d28 h1:pO0VjeSk0Tcd0NIHxgD6Gyd8T0pw79hs6Usr2Cwr16M= +github.com/bobuhiro11/gokvm v0.0.8-0.20231003020000-f53faca69d28/go.mod h1:xQjzvEq5CXolwHJyswTQXuGXNjF3bYavvXZXDZS+FTI= +github.com/bombsimon/wsl/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU= +github.com/bombsimon/wsl/v3 v3.4.0/go.mod h1:KkIB+TXkqy6MvK9BDZVbZxKNYsE1/oLRJbIFtf14qqo= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285 h1:Dr+ezPI5ivhMn/3WOoB86XzMhie146DNaBbhaQWZHMY= +github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM= github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ= +github.com/breml/bidichk v0.2.4 h1:i3yedFWWQ7YzjdZJHnPo9d/xURinSq3OM+gyM43K4/8= +github.com/breml/bidichk v0.2.4/go.mod h1:7Zk0kRFt1LIZxtQdl9W9JwGAcLTTkOs+tN7wuEYGJ3s= +github.com/breml/errchkjson v0.3.1 h1:hlIeXuspTyt8Y/UmP5qy1JocGNR00KQHgfaNtRAjoxQ= +github.com/breml/errchkjson v0.3.1/go.mod h1:XroxrzKjdiutFyW3nWhw34VGg7kiMsDQox73yWCGI2U= +github.com/butuzov/ireturn v0.2.0 h1:kCHi+YzC150GE98WFuZQu9yrTn6GEydO2AuPLbTgnO4= +github.com/butuzov/ireturn v0.2.0/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= +github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls= +github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g= github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets= +github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= +github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= +github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= @@ -859,14 +1352,23 @@ github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Y github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 h1:W9o46d2kbNL06lq7UNDPV0zYLzkrde/bjIqO02eoll0= +github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8/go.mod h1:gakxgyXaaPkxvLw1XQxNGK4I37ys9iBRzNUx/B7pUCo= +github.com/checkpoint-restore/go-criu/v6 v6.3.0 h1:mIdrSO2cPNWQY1truPg6uHLXyKHk3Z5Odx4wjKOASzA= +github.com/checkpoint-restore/go-criu/v6 v6.3.0/go.mod h1:rrRTN/uSwY2X+BPRl/gkulo9gsKOSAeVp9/K2tv7xZI= +github.com/cheggaaa/pb v1.0.27 h1:wIkZHkNfC7R6GI5w7l/PdAdzXzlrbcI3p8OAlnkTsnc= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI= +github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ= github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs= github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0= github.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= @@ -876,13 +1378,21 @@ github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyM github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -894,12 +1404,19 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/cockroach-go/v2 v2.1.1 h1:3XzfSMuUT0wBe1a3o5C0eOTcArhmmFAg2Jzh/7hhKqo= +github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= +github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo= +github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwuWwPHPYoCZ/KLAjHv5g4h2MS4f2/MTI= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4= github.com/coder/clistat v1.0.0 h1:MjiS7qQ1IobuSSgDnxcCSyBPESs44hExnh2TEqMcGnA= github.com/coder/clistat v1.0.0/go.mod h1:F+gLef+F9chVrleq808RBxdaoq52R4VLopuLdAsh8Y4= github.com/coder/flog v1.1.0 h1:kbAes1ai8fIS5OeV+QAnKBQE22ty1jRF/mcAwHpLBa4= github.com/coder/flog v1.1.0/go.mod h1:UQlQvrkJBvnRGo69Le8E24Tcl5SJleAAR7gYEHzAmdQ= +github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1 h1:UqBrPWSYvRI2s5RtOul20JukUEpu4ip9u7biBL+ntgk= github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322 h1:m0lPZjlQ7vdVpRBPKfYIFlmgevoTkBxB10wv6l2gOaU= github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322/go.mod h1:rOLFDDVKVFiDqZFXoteXc97YXx7kFi9kYqR+2ETPkLQ= @@ -935,25 +1452,73 @@ github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Au github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0/go.mod h1:qANbdpqyAGlo2bg+4gQKPj24H1ZWa3bQU2Q5/bV5B3Y= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818 h1:bNhUTaKl3q0bFn78bBRq7iIwo72kNTvUD9Ll5TTzDDk= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818/go.mod h1:fAlLM6hUgnf4Sagxn2Uy5Us0PBgOYWz+63HwHUVGEbw= +github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q= +github.com/confluentinc/confluent-kafka-go v1.9.2/go.mod h1:ptXNqsuDfYbAE/LBW6pnwWZElUoWxHoV8E43DCrliyo= +github.com/confluentinc/confluent-kafka-go/v2 v2.4.0 h1:NbOku86JJlsRJPJKE0snNsz6D1Qr4j5VR/lticrLZrY= +github.com/confluentinc/confluent-kafka-go/v2 v2.4.0/go.mod h1:E1dEQy50ZLfqs7T9luxz0rLxaeFZJZE92XvApJOr/Rk= +github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= +github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= +github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= +github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= +github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= +github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= +github.com/containerd/containerd/v2 v2.1.1 h1:znnkm7Ajz8lg8BcIPMhc/9yjBRN3B+OkNKqKisKfwwM= +github.com/containerd/containerd/v2 v2.1.1/go.mod h1:zIfkQj4RIodclYQkX7GSSswSwgP8d/XxDOtOAoSDIGU= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= +github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= +github.com/containerd/go-runc v1.0.0 h1:oU+lLv1ULm5taqgV/CJivypVODI4SUz1znWjv3nNYS0= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= +github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= +github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= +github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= +github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= +github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 h1:2Dx4IHfC1yHWI12AxQDJM1QbRCDfk6M+blLzlZCXdrc= +github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/daixiang0/gci v0.10.1 h1:eheNA3ljF6SxnPD/vE4lCBusVHmV3Rs3dkKvFrJ7MR0= +github.com/daixiang0/gci v0.10.1/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc= github.com/dave/dst v0.27.2/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk= @@ -964,6 +1529,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e h1:L+XrFvD0vBIBm+Wf9sFN6aU395t7JROoai0qXZraA4U= github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU= +github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= +github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= +github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= +github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= @@ -971,10 +1540,18 @@ github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= +github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ= +github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw= github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -983,22 +1560,57 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd h1:QMSNEh9uQkDjyPwu/J541GgSH+4hw+0skJDIj9HJ3mE= github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= +github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/eapache/go-resiliency v1.4.0 h1:3OK9bWpPk5q6pbFAaYSEwD9CLUSHG8bnZuqX2yMt3B0= +github.com/eapache/go-resiliency v1.4.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 h1:8EXxF+tCLqaVk8AOC29zl2mnhQjwyLxxOTuhUazWRsg= github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4/go.mod h1:I5sHm0Y0T1u5YjlyqC5GVArM7aNZRUYtTjmJ8mPJFds= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elastic/elastic-transport-go/v8 v8.1.0 h1:NeqEz1ty4RQz+TVbUrpSU7pZ48XkzGWQj02k5koahIE= +github.com/elastic/elastic-transport-go/v8 v8.1.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI= +github.com/elastic/go-elasticsearch/v6 v6.8.5 h1:U2HtkBseC1FNBmDr0TR2tKltL6FxoY+niDAlj5M8TK8= +github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= +github.com/elastic/go-elasticsearch/v7 v7.17.1 h1:49mHcHx7lpCL8cW1aioEwSEVKQF3s+Igi4Ye/QTWwmk= +github.com/elastic/go-elasticsearch/v7 v7.17.1/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4= +github.com/elastic/go-elasticsearch/v8 v8.4.0 h1:Rn1mcqaIMcNT43hnx2H62cIFZ+B6mjWtzj85BDKrvCE= +github.com/elastic/go-elasticsearch/v8 v8.4.0/go.mod h1:yY52i2Vj0unLz+N3Nwx1gM5LXwoj3h2dgptNGBYkMLA= github.com/elastic/go-sysinfo v1.15.1 h1:zBmTnFEXxIQ3iwcQuk7MzaUotmKRp3OabbbWM8TdzIQ= github.com/elastic/go-sysinfo v1.15.1/go.mod h1:jPSuTgXG+dhhh0GKIyI2Cso+w5lPJ5PvVqKlL8LV/Hk= github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY= @@ -1007,6 +1619,10 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1X github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.21.2 h1:OLDgvZKuofk4em9fT5tFG5j4jE1/hXnX75UMvcrL4AA= github.com/emersion/go-smtp v0.21.2/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/emicklei/go-restful v2.16.0+incompatible h1:rgqiKNjTnFQA6kkhFe16D8epTksy9HQ1MyrbDXSdYhM= +github.com/emicklei/go-restful v2.16.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -1034,8 +1650,20 @@ github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfU github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/esimonov/ifshort v1.0.4 h1:6SID4yGWfRae/M7hkVDVVyppy8q/v9OuxNdmjLQStBA= +github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0= +github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= +github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/evanw/esbuild v0.25.3 h1:4JKyUsm/nHDhpxis4IyWXAi8GiyTwG1WdEp6OhGVE8U= github.com/evanw/esbuild v0.25.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72 h1:0eU/faU2oDIB2BkQVM02hgRLJjGzzUuRf19HUhp0394= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -1044,13 +1672,22 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fergusstrange/embedded-postgres v1.31.0 h1:JmRxw2BcPRcU141nOEuGXbIU6jsh437cBB40rmftZSk= github.com/fergusstrange/embedded-postgres v1.31.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk= +github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= +github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= +github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e h1:Ss/B3/5wWRh8+emnK0++g5zQzwDTi30W10pKxKc4JXI= +github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e/go.mod h1:HyVoz1Mz5Co8TFO8EupIdlcpwShBmY98dkT2xeHkvEI= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -1060,20 +1697,34 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsouza/fake-gcs-server v1.17.0 h1:OeH75kBZcZa3ZE+zz/mFdJ2btt9FgqfjI7gIh9+5fvk= +github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg= +github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw= github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a h1:fwNLHrP5Rbg/mGSXCjtPdpbqv2GucVTA/KMi8wEm6mE= github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a/go.mod h1:/WeFVhhxMOGypVKS0w8DUJxUBbHypnWkUVnW7p5c9Pw= github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE= github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= @@ -1084,10 +1735,18 @@ github.com/go-chi/hostrouter v0.2.0 h1:GwC7TZz8+SlJN/tV/aeJgx4F+mI5+sp+5H1PelQUj github.com/go-chi/hostrouter v0.2.0/go.mod h1:pJ49vWVmtsKRKZivQx0YMYv4h0aX+Gcn6V23Np9Wf1s= github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= +github.com/go-critic/go-critic v0.8.0 h1:4zOcpvDoKvBOl+R1W81IBznr78f8YaE4zKXkfDVxGGA= +github.com/go-critic/go-critic v0.8.0/go.mod h1:5TjdkPI9cu/yKbYS96BTsslihjKd6zg6vd8O9RZXj2s= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= @@ -1095,16 +1754,23 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -1114,19 +1780,38 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4 github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= +github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= +github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pg/pg/v10 v10.11.1 h1:vYwbFpqoMpTDphnzIPshPPepdy3VpzD8qo29OFKp4vo= +github.com/go-pg/pg/v10 v10.11.1/go.mod h1:ExJWndhDNNftBdw1Ow83xqpSf4WMSJK8urmXD5VXS1I= +github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= +github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -1135,18 +1820,44 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= +github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= +github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= +github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= +github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= +github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= +github.com/go-toolsmith/astequal v1.1.0 h1:kHKm1AWqClYn15R0K1KKE4RG614D46n+nqUQ06E1dTw= +github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= +github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= +github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= +github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= +github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= +github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= +github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= +github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= +github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= +github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -1156,8 +1867,20 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.15.23 h1:WS0GAX1uNPDLUvLkNU2vXq6oTnsmfVFocjQ/4qA48qo= +github.com/goccy/go-yaml v1.15.23/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gocql/gocql v1.6.0 h1:IdFdOTbnpbd0pDhl4REKQDM+Q0SzKXQ1Yh+YZZ8T/qU= +github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= +github.com/gocsaf/csaf/v3 v3.2.0 h1:LF9j1ou4Cm5MT4+oHbk16Ae0hSmCztwPJqciTapVNIU= +github.com/gocsaf/csaf/v3 v3.2.0/go.mod h1:EpUCrQg69i+Y66MphmQvVbcj333GFLjXOYHg1zoXVso= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= +github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= @@ -1180,6 +1903,10 @@ github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XG github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0= +github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95 h1:sgew0XCnZwnzpWxTt3V8LLiCO7OQi3C6dycaE67wfkU= +github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95/go.mod h1:bOlVlCa1/RajcHpXkrUXPSHB/Re1UnlXxD1Qp8SKOd8= +github.com/gojuno/minimock/v3 v3.0.8 h1:+L+WvGoTvPB4YCbkMI5WFyp3Mvz6Z5ubBuTXWMhmwmA= +github.com/gojuno/minimock/v3 v3.0.8/go.mod h1:TPKxc8tiB8O83YH2//pOzxvEjaI3TMhd6ev/GmlMiYA= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= @@ -1187,6 +1914,11 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1226,16 +1958,45 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6J5HIP8ZtyMdiDscjMLfRBSPuzVVeo= +github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= +github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 h1:amWTbTGqOZ71ruzrdA+Nx5WA3tV1N0goTspwmKCQvBY= +github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2/go.mod h1:9wOXstvyDRshQ9LggQuzBCGysxs3b6Uo/1MvYCR2NMs= +github.com/golangci/golangci-lint v1.52.2 h1:FrPElUUI5rrHXg1mQ7KxI1MXPAw5lBVskiz7U7a8a1A= +github.com/golangci/golangci-lint v1.52.2/go.mod h1:S5fhC5sHM5kE22/HcATKd1XLWQxX+y7mHj8B5H91Q/0= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.4.0 h1:KtVB/hTK4bbL/S6bs64rYyk8adjmh1BygbBiaAiX+a0= +github.com/golangci/misspell v0.4.0/go.mod h1:W6O/bwV6lGDxUCChm2ykw9NQdd5bYd1Xkjo88UcWyJc= +github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 h1:DIPQnGy2Gv2FSA4B/hh8Q7xx3B7AIDk3DAMeHclH1vQ= +github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6/go.mod h1:0AKcRCkMoKvUvlf89F6O7H2LYdhr1zBh736mBItOdRs= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 h1:4txT5G2kqVAKMjzidIabL/8KqjIK71yj30YOeuxLn10= github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/certificate-transparency-go v1.1.8 h1:LGYKkgZF7satzgTak9R4yzfJXEeYVAjV6/EAEJOf1to= +github.com/google/certificate-transparency-go v1.1.8/go.mod h1:bV/o8r0TBKRf1X//iiiSgWrvII4d7/8OiA+3vG26gI8= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= +github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -1254,16 +2015,34 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= +github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= +github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= +github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= +github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 h1:DdHws/YnnPrSywrjNYu2lEHqYHWp/LnEx56w59esd54= github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405/go.mod h1:4RgUDSnsxP19d65zJWqvqJ/poJxBCvmna50eXmIvoR8= +github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= +github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= +github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-tpm v0.9.1-0.20230914180155-ee6cbcd136f8 h1:g9RVRZdQrNEK2E94RcFescvXFC9afWsFar4IIdejP34= +github.com/google/go-tpm v0.9.1-0.20230914180155-ee6cbcd136f8/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4= +github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= +github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA= +github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -1290,15 +2069,24 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/rpmpack v0.0.0-20221120200012-98b63d62fd77 h1:+C0+foB1Bm0WYdbaDIuUGEVG1Eqx9WWcGUoJBSLdZo0= +github.com/google/rpmpack v0.0.0-20221120200012-98b63d62fd77/go.mod h1:Ys71Xf3d5OC4Ww06S5YJcbk+Eh/1CsB+rZibR7NDEA4= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= +github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= +github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= @@ -1319,20 +2107,62 @@ github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57Q github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 h1:9alfqbrhuD+9fLZ4iaAVwhlp5PEhmnBt7yvK2Oy5C1U= +github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/goreleaser/chglog v0.4.2 h1:afmbT1d7lX/q+GF8wv3a1Dofs2j/Y9YkiCpGemWR6mI= +github.com/goreleaser/chglog v0.4.2/go.mod h1:u/F03un4hMCQrp65qSWCkkC6T+G7YLKZ+AM2mITE47s= +github.com/goreleaser/fileglob v0.3.1 h1:OTFDWqUUHjQazk2N5GdUqEbqT/grBnRARaAXsV07q1Y= +github.com/goreleaser/fileglob v0.3.1/go.mod h1:kNcPrPzjCp+Ox3jmXLU5QEsjhqrtLBm6OnXAif8KRl8= +github.com/goreleaser/nfpm v1.10.3 h1:NzpWKKzSFr7JOn55XN0SskyFOjP6BkvRt3JujoX8fws= +github.com/goreleaser/nfpm v1.10.3/go.mod h1:EEC7YD5wi+ol0MiAshpgPANBOkjXDl7wqTLVk68OBsk= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/forcetypeassert v0.1.0 h1:6eUflI3DiGusXGK6X7cCcIgVCpZ2CiZ1Q7jl6ZxNV70= +github.com/gostaticanalysis/forcetypeassert v0.1.0/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= +github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= +github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= +github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc= +github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= +github.com/graphql-go/handler v0.2.3 h1:CANh8WPnl5M9uA25c2GBhPqJhE53Fg0Iue/fRNla71E= +github.com/graphql-go/handler v0.2.3/go.mod h1:leLF6RpV5uZMN1CdImAxuiayrYYhOk33bZciaUGaXeU= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo= github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q= +github.com/hanwen/go-fuse/v2 v2.3.0 h1:t5ivNIH2PK+zw4OBul/iJjsoG9K6kXo4nMDoBpciC8A= +github.com/hanwen/go-fuse/v2 v2.3.0/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= +github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU= +github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU= +github.com/hashicorp/consul/api v1.24.0 h1:u2XyStA2j0jnCiVUU7Qyrt8idjRn4ORhK6DlvZ3bWhA= +github.com/hashicorp/consul/api v1.24.0/go.mod h1:NZJGRFYruc/80wYowkPFCp1LbGmJC9L8izrwfyVx/Wg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -1346,6 +2176,8 @@ github.com/hashicorp/go-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgM github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= @@ -1355,6 +2187,8 @@ github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b h1:3GrpnZQBxcMj1 github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b/go.mod h1:qIFzeFcJU3OIFk/7JreWXcUjFmcCaeHTH9KoNyHYVCs= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= @@ -1373,6 +2207,7 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= @@ -1383,6 +2218,8 @@ github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3q github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= @@ -1397,6 +2234,10 @@ github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwD github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= +github.com/hashicorp/vault/api v1.9.2 h1:YjkZLJ7K3inKgMZ0wzCU9OHqc+UqMQyXsPXnf3Cl2as= +github.com/hashicorp/vault/api v1.9.2/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= +github.com/hashicorp/vault/sdk v0.9.2 h1:H1kitfl1rG2SHbeGEyvhEqmIjVKE3E6c2q3ViKOs6HA= +github.com/hashicorp/vault/sdk v0.9.2/go.mod h1:gG0lA7P++KefplzvcD3vrfCmgxVAM7Z/SqX5NeOL/98= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU= @@ -1405,24 +2246,91 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hugelgupf/go-shlex v0.0.0-20200702092117-c80c9d0918fa h1:s3KPo0nThtvjEamF/aElD4k5jSsBHew3/sgNTnth+2M= +github.com/hugelgupf/go-shlex v0.0.0-20200702092117-c80c9d0918fa/go.mod h1:I1uW6ymzwsy5TlQgD1bFAghdMgBYqH1qtCeHoZgHMqs= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1 h1:jWoR2Yqg8tzM0v6LAiP7i1bikZJu3gxpgvu3g1Lw+a0= github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= +github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c h1:gYfYE403/nlrGNYj6BEOs9ucLCAGB9gstlSk92DttTg= +github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= +github.com/intel-go/cpuid v0.0.0-20200819041909-2aa72927c3e2 h1:h+RKaNPjka7LRJGoeub/IQBdXSoEaJjfADkBq02hvjw= +github.com/intel-go/cpuid v0.0.0-20200819041909-2aa72927c3e2/go.mod h1:RmeVYf9XrPRbRc3XIx0gLYA8qOFvNoPOfaEZduRlEp4= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jgautheron/goconst v1.5.1 h1:HxVbL1MhydKs8R8n/HE5NPvzfaYmQJA3o879lE4+WcM= +github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= +github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -1434,16 +2342,34 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA= github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= +github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/junk1tm/musttag v0.5.0 h1:bV1DTdi38Hi4pG4OVWa7Kap0hi0o7EczuK6wQt9zPOM= +github.com/junk1tm/musttag v0.5.0/go.mod h1:PcR7BA+oREQYvHwgjIDmw3exJeds5JzRcvEJTfjrA0M= github.com/justinas/nosurf v1.2.0 h1:yMs1bSRrNiwXk4AS6n8vL2Ssgpb9CB25T/4xrixaK0s= github.com/justinas/nosurf v1.2.0/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= +github.com/k0kubun/pp v2.3.0+incompatible h1:EKhKbi34VQDWJtq+zpsKSEhkHHs9w2P8Izbq8IhLVSo= +github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kaey/framebuffer v0.0.0-20140402104929-7b385489a1ff h1:eK9dwGbaOkNQ44GPFD571d+51Qa2AGIYAR3kpjsrhLE= +github.com/kaey/framebuffer v0.0.0-20140402104929-7b385489a1ff/go.mod h1:tS4qtlcKqtt3tCIHUflVSqeP3CLH5Qtv2szX9X2SyhU= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= +github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -1451,7 +2377,13 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/errcheck v1.6.3 h1:dEKh+GLHcWm2oN34nMvDzn1sqI0i0WxPvrgiJA5JuM8= +github.com/kisielk/errcheck v1.6.3/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8= +github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= @@ -1460,6 +2392,22 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg= +github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8= +github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 h1:dWzdsqjh1p2gNtRKqNwuBvKqMNwnLOPLzVZT1n6DK7s= +github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23/go.mod h1:lUaIXCWzf7BRKTY5iEcrYy1TfgbYLYVIS/B2vPkJzOc= +github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 h1:aC6MEAs3PE3lWD7lqrJfDxHd6hcced9R4JTZu85cJwU= +github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= +github.com/knqyf263/go-rpmdb v0.1.1 h1:oh68mTCvp1XzxdU7EfafcWzzfstUZAEa3MW0IJye584= +github.com/knqyf263/go-rpmdb v0.1.1/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww= +github.com/knqyf263/labeler v0.0.0-20200423181506-7a6e545148c3 h1:AvRd4VDhlo8/opGjS79zaVmIfoZzEOtBV1PvSRUkC7o= +github.com/knqyf263/labeler v0.0.0-20200423181506-7a6e545148c3/go.mod h1:DfoJpLAw0HeB4cYJFg1S8LqtYDvSKv3rh3wGAFwv5Bg= +github.com/knqyf263/nested v0.0.1 h1:Sv26CegUMhjt19zqbBKntjwESdxe5hxVPSk0+AKjdUc= +github.com/knqyf263/nested v0.0.1/go.mod h1:zwhsIhMkBg90DTOJQvxPkKIypEHPYkgWHs4gybdlUmk= +github.com/knz/bubbline v0.0.0-20230717192058-486954f9953f h1:Jjq9RwAsihYCm5fudPBj5CwsLDNd6r1ee3M2mfKFYdI= +github.com/knz/bubbline v0.0.0-20230717192058-486954f9953f/go.mod h1:ucXvyrucVy4jp/4afdKWNW1TVO73GMI72VNINzyT678= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -1469,45 +2417,117 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylecarbs/aisdk-go v0.0.8 h1:hnKVbLM6U8XqX3t5I26J8k5saXdra595bGt1HP0PvKA= -github.com/kylecarbs/aisdk-go v0.0.8/go.mod h1:3nAhClwRNo6ZfU44GrBZ8O2fCCrxJdaHb9JIz+P3LR8= +github.com/ktrysmt/go-bitbucket v0.6.4 h1:C8dUGp0qkwncKtAnozHCbbqhptefzEd1I0sfnuy9rYQ= +github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= +github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= +github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= +github.com/kunwardeep/paralleltest v1.0.6 h1:FCKYMF1OF2+RveWlABsdnmsvJrei5aoyZoaGS+Ugg8g= +github.com/kunwardeep/paralleltest v1.0.6/go.mod h1:Y0Y0XISdZM5IKm3TREQMZ6iteqn1YuwCsJO/0kL9Zes= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3 h1:Z9/bo5PSeMutpdiKYNt/TTSfGM1Ll0naj3QzYX9VxTc= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= +github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA= github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8 h1:Y7O3Z3YeNRtw14QrtHpevU4dSjCkov0J40MtQ7Nc0n8= github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8/go.mod h1:n/KX1BZoN1m9EwoXkn/xAV4fd3k8c++gGBsgLONaPOY= github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e h1:OP0ZMFeZkUnOzTFRfpuK3m7Kp4fNvC6qN+exwj7aI4M= github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= +github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= +github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= +github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= +github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA= +github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= +github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= +github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/leeavital/protoc-gen-gostreamer v0.1.0 h1:YYsSCUeNK4/0F69IkG916qYDAb4BxTH3kg8cC273cu4= +github.com/leeavital/protoc-gen-gostreamer v0.1.0/go.mod h1:sC19nxpNkHy3enGT3ck6LTr5mittUoUXE/elp/mnTS4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= +github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= +github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ= +github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= +github.com/liamg/iamgo v0.0.9 h1:tADGm3xVotyRJmuKKaH4+zsBn7LOcvgdpuF3WsSKW3c= +github.com/liamg/iamgo v0.0.9/go.mod h1:Kk6ZxBF/GQqG9nnaUjIi6jf+WXNpeOTyhwc6gnguaZQ= +github.com/liamg/jfather v0.0.7 h1:Xf78zS263yfT+xr2VSo6+kyAy4ROlCacRqJG7s5jt4k= +github.com/liamg/jfather v0.0.7/go.mod h1:xXBGiBoiZ6tmHhfy5Jzw8sugzajwYdi6VosIpB3/cPM= github.com/liamg/memoryfs v1.6.0 h1:jAFec2HI1PgMTem5gR7UT8zi9u4BfG5jorCRlLH06W8= github.com/liamg/memoryfs v1.6.0/go.mod h1:z7mfqXFQS8eSeBBsFjYLlxYRMRyiPktytvYCYTb3BSk= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= +github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI= github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= +github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= +github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= +github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= +github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0= github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA= github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4= github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= +github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMASY= +github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/masahiro331/go-disk v0.0.0-20240625071113-56c933208fee h1:cgm8mE25x5XXX2oyvJDlyJ72K+rDu/4ZCYce2worNb8= +github.com/masahiro331/go-disk v0.0.0-20240625071113-56c933208fee/go.mod h1:rojbW5tVhH1cuVYFKZS+QX+VGXK45JVsRO+jW92kkKM= +github.com/masahiro331/go-ebs-file v0.0.0-20240917043618-e6d2bea5c32e h1:nCgF1JEYIS8KNuJtIeUrmjjhktIMKWNmASZqwK2ynu0= +github.com/masahiro331/go-ebs-file v0.0.0-20240917043618-e6d2bea5c32e/go.mod h1:XFWPTlAcEL733RUjbr0QBybdt6oK2DH7LZk8id2qtd4= +github.com/masahiro331/go-ext4-filesystem v0.0.0-20240620024024-ca14e6327bbd h1:JEIW94K3spsvBI5Xb9PGhKSIza9/jxO1lF30tPCAJlA= +github.com/masahiro331/go-ext4-filesystem v0.0.0-20240620024024-ca14e6327bbd/go.mod h1:3XMMY1M486mWGTD13WPItg6FsgflQR72ZMAkd+gsyoQ= +github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a h1:eLvAzVoRfHEOl64OxFhepPf3vj7SKvXY/tFc3BS0b7s= +github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a/go.mod h1:jZ3F25l7DbD7l7DcA8aj7eo1EZ84nbzcQHBB4lCSrI8= +github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd h1:Y30EzvuoVp97b0unb/GOFXzBUKRXZXUN2e0wYmvC+ic= +github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd/go.mod h1:5f7mCJGW9cJb8SDn3z8qodGxpMCOo8d/2nls/tiwRrw= +github.com/masahiro331/go-xfs-filesystem v0.0.0-20231205045356-1b22259a6c44 h1:VmSjn0UCyfXUNdePDr7uM/uZTnGSp+mKD5+cYkEoLx4= +github.com/masahiro331/go-xfs-filesystem v0.0.0-20231205045356-1b22259a6c44/go.mod h1:QKBZqdn6teT0LK3QhAf3K6xakItd1LonOShOEC44idQ= +github.com/masterminds/semver v1.5.0 h1:hTxJTTY7tjvnWMrl08O6u3G6BLlKVwxSz01lVac9P8U= +github.com/masterminds/semver v1.5.0/go.mod h1:s7KNT9fnd7edGzwwP7RBX4H0v/CYd5qdOLfkL1V75yg= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= +github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a h1:+J2gw7Bw77w/fbK7wnNJJDKmw1IbWft2Ul5BzrG1Qm8= +github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -1527,23 +2547,40 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo= +github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= +github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= +github.com/mgechev/revive v1.3.1 h1:OlQkcH40IB2cGuprTPcjB0iIUddgVZgGmDX3IAMR8D4= +github.com/mgechev/revive v1.3.1/go.mod h1:YlD6TTWl2B8A103R9KWJSPVI9DrEf+oqr15q21Ld+5I= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/microsoft/go-mssqldb v1.0.0 h1:k2p2uuG8T5T/7Hp7/e3vMGTnnR0sU4h8d1CcC71iLHU= +github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -1556,21 +2593,35 @@ github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/buildkit v0.21.1 h1:wTjVLfirh7skZt9piaIlNo8WdiPjza1CDl2EArDV9bA= +github.com/moby/buildkit v0.21.1/go.mod h1:mBq0D44uCyz2PdX8T/qym5LBbkBO3GGv0wqgX9ABYYw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/moby v28.2.2+incompatible h1:sBNZudYVackyiyn2yoBUpAoRcDun9bnUCozAW6lAnPs= github.com/moby/moby v28.2.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= +github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= @@ -1586,8 +2637,18 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= +github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm/Q= +github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -1600,60 +2661,125 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mutecomm/go-sqlcipher/v4 v4.4.0 h1:sV1tWCWGAVlPhNGT95Q+z/txFxuhAYWwHD1afF5bMZg= +github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/nanmu42/limitio v1.0.0 h1:dpopBYPwUyLOPv+vsGja0iax+dG0SP9paTEmz+Sy7KU= +github.com/nanmu42/limitio v1.0.0/go.mod h1:8H40zQ7pqxzbwZ9jxsK2hDoE06TH5ziybtApt1io8So= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA= +github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba h1:fhFP5RliM2HW/8XdcO5QngSfFli9GcRIpMXvypTQt6E= +github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= +github.com/nishanths/exhaustive v0.10.0 h1:BMznKAcVa9WOoLq/kTGp4NJOJSMwEpcpjFNAVRfPlSo= +github.com/nishanths/exhaustive v0.10.0/go.mod h1:IbwrGdVMizvDcIxPYGVdQn5BqWJaOwpCvg4RGb8r/TA= +github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= +github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= +github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= +github.com/nunnatsa/ginkgolinter v0.11.2 h1:xzQpAsEyZe5F1RMy2Z5kn8UFCGiWfKqJOUd2ZzBXA4M= +github.com/nunnatsa/ginkgolinter v0.11.2/go.mod h1:dJIGXYXbkBswqa/pIzG0QlVTTDSBMxDoCFwhsl4Uras= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/open-policy-agent/opa v1.4.2 h1:ag4upP7zMsa4WE2p1pwAFeG4Pn3mNwfAx9DLhhJfbjU= github.com/open-policy-agent/opa v1.4.2/go.mod h1:DNzZPKqKh4U0n0ANxcCVlw8lCSv2c+h5G/3QvSYdWZ8= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 h1:lK/3zr73guK9apbXTcnDnYrC0YCQ25V3CIULYz3k2xU= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1/go.mod h1:01TvyaK8x640crO2iFwW/6CFCZgNsOvOGH3B5J239m0= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1 h1:TCyOus9tym82PD1VYtthLKMVMlVyRwtDI4ck4SR2+Ok= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1/go.mod h1:Z/S1brD5gU2Ntht/bHxBVnGxXKTvZDr0dNv/riUzPmY= -github.com/openai/openai-go v0.1.0-beta.10 h1:CknhGXe8aXQMRuqg255PFnWzgRY9nEryMxoNIBBM9tU= -github.com/openai/openai-go v0.1.0-beta.10/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/openai/openai-go v1.3.0 h1:lBpvgXxGHUufk9DNTguval40y2oK0GHZwgWQyUtjPIQ= +github.com/openai/openai-go v1.3.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= +github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= +github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= +github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openvex/discovery v0.1.1-0.20240802171711-7c54efc57553 h1:c4u0GIH0w2Q57Pm2Oldrq6EiHFnLCCnRs98A+ggj/YQ= +github.com/openvex/discovery v0.1.1-0.20240802171711-7c54efc57553/go.mod h1:z4b//Qi7p7zcM/c41ogeTy+/nqfMbbeYnfZ+EMCTCD0= +github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ= +github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo= +github.com/orangecms/go-framebuffer v0.0.0-20200613202404-a0700d90c330 h1:zJBTzBuTR7EdFzmCGx0xp0pbOOb82sAh0+YUK4JTDEI= +github.com/orangecms/go-framebuffer v0.0.0-20200613202404-a0700d90c330/go.mod h1:3Myb/UszJY32F2G7yGkUtcW/ejHpjlGfYLim7cv2uKA= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0= github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= +github.com/owenrumney/go-sarif/v2 v2.3.3 h1:ubWDJcF5i3L/EIOER+ZyQ03IfplbSU1BLOE26uKQIIU= +github.com/owenrumney/go-sarif/v2 v2.3.3/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= +github.com/owenrumney/squealer v1.2.11 h1:vMudrj70VeOzY+t7Phz9Yo0wAgm4kXes9DcTLBVDqGY= +github.com/owenrumney/squealer v1.2.11/go.mod h1:8KOuitfOfmS/OtzgxQbxnnrbngAGopfgKB/BiGGpqGA= +github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= +github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= 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/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmOEA= +github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/peterbourgon/ff/v3 v3.3.0 h1:PaKe7GW8orVFh8Unb5jNHS+JZBwWUMa2se0HM6/BI24= +github.com/peterbourgon/ff/v3 v3.3.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= +github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= @@ -1671,6 +2797,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -1678,7 +2806,11 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polyfloyd/go-errorlint v1.4.1 h1:r8ru5FhXSn34YU1GJDOuoJv2LdsQkPmK325EOpPMJlM= +github.com/polyfloyd/go-errorlint v1.4.1/go.mod h1:k6fU/+fQe38ednoZS51T7gSIGQW1y94d6TkSr35OzH8= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA= @@ -1696,10 +2828,26 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/quasilyte/go-ruleguard v0.3.19 h1:tfMnabXle/HzOb5Xe9CUZYWXKfkS1KwRmZyPmD9nVcc= +github.com/quasilyte/go-ruleguard v0.3.19/go.mod h1:lHSn69Scl48I7Gt9cX3VrbsZYvYiBYszZOZW4A+oTEw= github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= +github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/rck/unit v0.0.3 h1:q3/Ui9gcrFKpEneZXw2gNmNEbzv5jLrZnH6qhX1ypZ0= +github.com/rck/unit v0.0.3/go.mod h1:jTOnzP4s1OjIP1vdxb4n76b23QPKS4EurYg7sYMr2DM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= +github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= +github.com/redis/rueidis v1.0.56 h1:DwPjFIgas1OMU/uCqBELOonu9TKMYt3MFPq6GtwEWNY= +github.com/redis/rueidis v1.0.56/go.mod h1:g660/008FMYmAF46HG4lmcpcgFNj+jCjCAZUUM+wEbs= +github.com/rekby/gpt v0.0.0-20200219180433-a930afbc6edc h1:goZGTwEEn8mWLcY012VouWZWkJ8GrXm9tS3VORMxT90= +github.com/rekby/gpt v0.0.0-20200219180433-a930afbc6edc/go.mod h1:scrOqOnnHVKCHENvFw8k9ajCb88uqLQDA4BvuJNJ2ew= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -1713,47 +2861,146 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79 h1:V7x0hCAgL8lNGezuex1RW1sh7VXXCqfw8nXZti66iFg= +github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg= +github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= +github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +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/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c h1:8gOLsYwaY2JwlTMT4brS5/9XJdrdIbmk2obvQ748CC0= +github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c/go.mod h1:kwM/7r/rVluTE8qJbHAffduuqmSv4knVQT2IajGvSiA= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw= +github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= +github.com/ryanrolds/sqlclosecheck v0.4.0 h1:i8SX60Rppc1wRuyQjMciLqIzV3xnoHB7/tXbr6RGYNI= +github.com/ryanrolds/sqlclosecheck v0.4.0/go.mod h1:TBRRjzL31JONc9i4XMinicuo+s+E8yKZ5FN8X3G6CKQ= +github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/safchain/ethtool v0.0.0-20200218184317-f459e2d13664 h1:gvolwzuDhul9qK6/oHqxCHD5TEYfsWNBGidOeG6kvpk= +github.com/safchain/ethtool v0.0.0-20200218184317-f459e2d13664/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= +github.com/samber/oops v1.15.0 h1:/mF33KAqA2TugU6y/tomFpK6G6mJB7g0aqRyHkaSIeg= +github.com/samber/oops v1.15.0/go.mod h1:9LpLZkpjojEt/of7EpG5o65i/Lp23ddDvGhg2L871Ow= +github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= +github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= +github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.23.0 h1:01h+/2Kd+NblNItNeux0veSL5cBF1jbEOPrEhDzGYq0= +github.com/sashamelentyev/usestdlibvars v1.23.0/go.mod h1:YPwr/Y1LATzHI93CqoPUN/2BzGQ/6N/cl/KwgR0B/aU= +github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= +github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= +github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc= +github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= +github.com/seccomp/libseccomp-golang v0.10.0 h1:aA4bp+/Zzi0BnWZ2F1wgNBs5gTpm+na2rWM6M9YjLpY= +github.com/seccomp/libseccomp-golang v0.10.0/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= +github.com/securego/gosec/v2 v2.15.0 h1:v4Ym7FF58/jlykYmmhZ7mTm7FQvN/setNm++0fgIAtw= +github.com/securego/gosec/v2 v2.15.0/go.mod h1:VOjTrZOkUtSDt2QLSJmQBMWnvwiQPEjg0l+5juIqGk8= +github.com/segmentio/kafka-go v0.4.42 h1:qffhBZCz4WcWyNuHEclHjIMLs2slp6mZO8px+5W5tfU= +github.com/segmentio/kafka-go v0.4.42/go.mod h1:d0g15xPMqoUookug0OU75DhGZxXwCFxSLeJ4uphwJzg= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= +github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk= github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sigstore/cosign/v2 v2.2.4 h1:iY4vtEacmu2hkNj1Fh+8EBqBwKs2DHM27/lbNWDFJro= +github.com/sigstore/cosign/v2 v2.2.4/go.mod h1:JZlRD2uaEjVAvZ1XJ3QkkZJhTqSDVtLaet+C/TMR81Y= +github.com/sigstore/protobuf-specs v0.4.1 h1:5SsMqZbdkcO/DNHudaxuCUEjj6x29tS2Xby1BxGU7Zc= +github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/rekor v1.3.10 h1:/mSvRo4MZ/59ECIlARhyykAlQlkmeAQpvBPlmJtZOCU= +github.com/sigstore/rekor v1.3.10/go.mod h1:JvryKJ40O0XA48MdzYUPu0y4fyvqt0C4iSY7ri9iu3A= +github.com/sigstore/sigstore v1.9.1 h1:bNMsfFATsMPaagcf+uppLk4C9rQZ2dh5ysmCxQBYWaw= +github.com/sigstore/sigstore v1.9.1/go.mod h1:zUoATYzR1J3rLNp3jmp4fzIJtWdhC3ZM6MnpcBtnsE4= +github.com/sigstore/timestamp-authority v1.2.2 h1:X4qyutnCQqJ0apMewFyx+3t7Tws00JQ/JonBiu3QvLE= +github.com/sigstore/timestamp-authority v1.2.2/go.mod h1:nEah4Eq4wpliDjlY342rXclGSO7Kb9hoRrl9tqLW13A= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= +github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= +github.com/sivchari/nosnakecase v1.7.0 h1:7QkpWIRMe8x25gckkFd2A5Pi6Ymo0qgr4JrhGt95do8= +github.com/sivchari/nosnakecase v1.7.0/go.mod h1:CwDzrzPea40/GB6uynrNLiorAlgFRvRbFSgJx2Gs+QY= +github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= +github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/snowflakedb/gosnowflake v1.6.19 h1:KSHXrQ5o7uso25hNIzi/RObXtnSGkFgie91X82KcvMY= +github.com/snowflakedb/gosnowflake v1.6.19/go.mod h1:FM1+PWUdwB9udFDsXdfD58NONC0m+MlOSmQRvimobSM= +github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= +github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= github.com/sosedoff/gitkit v0.4.0 h1:opyQJ/h9xMRLsz2ca/2CRXtstePcpldiZN8DpLLF8Os= github.com/sosedoff/gitkit v0.4.0/go.mod h1:V3EpGZ0nvCBhXerPsbDeqtyReNb48cwP9KtkUYTKT5I= +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/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= +github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/fsync v0.10.1 h1:JRnB7G72b+gIBaBcpn5ibJSd7ww1iEahXSX2B8G6dSE= +github.com/spf13/fsync v0.10.1/go.mod h1:y+B41vYq5i6Boa3Z+BVoPbDeOvxVkNU5OBXhoT8i4TQ= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= +github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= +github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -1774,6 +3021,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= @@ -1782,22 +3031,40 @@ github.com/swaggo/http-swagger/v2 v2.0.1 h1:mNOBLxDjSNwCKlMxcErjjvct/xhc9t2KIO48 github.com/swaggo/http-swagger/v2 v2.0.1/go.mod h1:XYhrQVIKz13CxuKD4p4kvpaRB4jJ1/MlfQXVOE+CX8Y= github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= +github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d h1:K3j02b5j2Iw1xoggN9B2DIEkhWGheqFOeDkdJdBrJI8= github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs= +github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HPjrSuJYEkdZ+0ItmGQAQ75cRHIiftIyE= +github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= +github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns= +github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ= github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ= github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e/go.mod h1:DjoeCULdP6vTJ/xY+nzzR9LaUHprkbZEpNidX0aqEEk= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= +github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 h1:7xU7AFQE83h0wz/dIMvD0t77g0FxFfZIQjghDQxyG2U= +github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89/go.mod h1:OGMqrTzDqmJkGumUTtOv44Rp3/4xS+QFbE8Rn0AGlaU= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= +github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw= github.com/tdewolff/minify/v2 v2.20.37/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU= github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw= @@ -1809,44 +3076,116 @@ github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= github.com/testcontainers/testcontainers-go/modules/localstack v0.37.0 h1:nPuxUYseqS0eYJg7KDJd95PhoMhdpTnSNtkDLwWFngo= github.com/testcontainers/testcontainers-go/modules/localstack v0.37.0/go.mod h1:Mw+N4qqJ5iWbg45yWsdLzICfeCEwvYNudfAHHFqCU8Q= +github.com/tetafro/godot v1.4.11 h1:BVoBIqAf/2QdbFmSwAWnaIqDivZdOV0ZRwEm6jivLKw= +github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= +github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA= +github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= +github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= +github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= +github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= +github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= +github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/tomarrell/wrapcheck/v2 v2.8.1 h1:HxSqDSN0sAt0yJYsrcYVoEeyM4aI9yAm3KQpIXDJRhQ= +github.com/tomarrell/wrapcheck/v2 v2.8.1/go.mod h1:/n2Q3NZ4XFT50ho6Hbxg+RV1uyo2Uow/Vdm9NQcl5SE= +github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= +github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/tonglil/versioning v0.0.0-20170205083536-8b2a4334bd1d h1:3H+wrTJTy3PVEeCyrjiCWjrh7pVEodGgJgA8Q1tpcbg= +github.com/tonglil/versioning v0.0.0-20170205083536-8b2a4334bd1d/go.mod h1:/jU0OcDkhtRrbaJPiG/p3X7XOP1pkFWLvUbsnQKP6hY= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= +github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= +github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= +github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= +github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a h1:eg5FkNoQp76ZsswyGZ+TjYqA/rhKefxK8BW7XOlQsxo= github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a/go.mod h1:e/8TmrdreH0sZOw2DFKBaUV7bvDWRq6SeM9PzkuVM68= +github.com/u-root/iscsinl v0.1.1-0.20210528121423-84c32645822a h1:A0sK7WEodak7eVd21MOEatnh2pfAAwZaEPSIEEsjctQ= +github.com/u-root/iscsinl v0.1.1-0.20210528121423-84c32645822a/go.mod h1:RWIgJWqm9/0gjBZ0Hl8iR6MVGzZ+yAda2uqqLmetE2I= +github.com/u-root/mkuimage v0.0.0-20240225063926-11a3bcc79c2a h1:PsI71z55OoumrXP01sYr+cV3Ab4pL7Y2QW/ftMKG7CY= +github.com/u-root/mkuimage v0.0.0-20240225063926-11a3bcc79c2a/go.mod h1:Yqr8aXRStz71Z1JVz2bUut8Xt9wmBijQIVOSn3eYEIw= github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA= +github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= +github.com/ultraware/whitespace v0.0.5 h1:hh+/cpIcopyMYbZNVov9iSxvJU3OYQg78Sfaqzi/CzI= +github.com/ultraware/whitespace v0.0.5/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/uptrace/bun v1.1.17 h1:qxBaEIo0hC/8O3O6GrMDKxqyT+mw5/s0Pn/n6xjyGIk= +github.com/uptrace/bun v1.1.17/go.mod h1:hATAzivtTIRsSJR4B8AXR+uABqnQxr3myKDKEf5iQ9U= +github.com/uptrace/bun/dialect/sqlitedialect v1.1.17 h1:i8NFU9r8YuavNFaYlNqi4ppn+MgoHtqLgpWQDrVTjm0= +github.com/uptrace/bun/dialect/sqlitedialect v1.1.17/go.mod h1:YF0FO4VVnY9GHNH6rM4r3STlVEBxkOc6L88Bm5X5mzA= +github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= +github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/uudashr/gocognit v1.0.6 h1:2Cgi6MweCsdB6kpcVQp7EW4U23iBFQWfTXiWlyp842Y= +github.com/uudashr/gocognit v1.0.6/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY= +github.com/valkey-io/valkey-go v1.0.56 h1:7qp/9dqqPbYEEKeFZCnpX6nzM5XzO2MPp0iKh9+c9Wg= +github.com/valkey-io/valkey-go v1.0.56/go.mod h1:sxpCChk8i3oTG+A/lUi9Lj8C/7WI+yhnQCvDJlPVKNM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= +github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= +github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8= +github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= +github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -1858,13 +3197,24 @@ github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vb github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/vtolstov/go-ioctl v0.0.0-20151206205506-6be9cced4810 h1:X6ps8XHfpQjw8dUStzlMi2ybiKQ2Fmdw7UM+TinwvyM= +github.com/vtolstov/go-ioctl v0.0.0-20151206205506-6be9cced4810/go.mod h1:dF0BBJ2YrV1+2eAIyEI+KeSidgA6HqoIP1u5XTlMq/o= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= +github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= +github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -1872,16 +3222,28 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +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/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= +github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= +github.com/yeya24/promlinter v0.2.0/go.mod h1:u54lkmBOZrpEbQQ6gox2zWKKLKu2SGe+2KOiextY+IA= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= @@ -1897,6 +3259,8 @@ github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= @@ -1911,6 +3275,20 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= +github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +gitlab.com/bosi/decorder v0.2.3 h1:gX4/RgK16ijY8V+BRQHAySfQAb354T7/xQpDB2n10P0= +gitlab.com/bosi/decorder v0.2.3/go.mod h1:9K1RB5+VPNQYtXtTDAzd2OEftsZb1oV0IrJrzChSdGE= +gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= +gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= +go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M= +go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= +go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.nhat.io/otelsql v0.15.0 h1:e2lpIaFPe62Pa1fXZoOWXTvMzcN4SwHwHdCz1wDUG6c= @@ -1923,6 +3301,8 @@ go.opentelemetry.io/collector/component/componentstatus v0.120.0 h1:hzKjI9+AIl8A go.opentelemetry.io/collector/component/componentstatus v0.120.0/go.mod h1:kbuAEddxvcyjGLXGmys3nckAj4jTGC0IqDIEXAOr3Ag= go.opentelemetry.io/collector/component/componenttest v0.120.0 h1:vKX85d3lpxj/RoiFQNvmIpX9lOS80FY5svzOYUyeYX0= go.opentelemetry.io/collector/component/componenttest v0.120.0/go.mod h1:QDLboWF2akEqAGyvje8Hc7GfXcrZvQ5FhmlWvD5SkzY= +go.opentelemetry.io/collector/config/configtelemetry v0.119.0 h1:gAgMUEVXZKgpASxOrhS55DyA/aYatq0U6gitZI8MLXw= +go.opentelemetry.io/collector/config/configtelemetry v0.119.0/go.mod h1:SlBEwQg0qly75rXZ6W1Ig8jN25KBVBkFIIAUI1GiAAE= go.opentelemetry.io/collector/consumer v1.26.0 h1:0MwuzkWFLOm13qJvwW85QkoavnGpR4ZObqCs9g1XAvk= go.opentelemetry.io/collector/consumer v1.26.0/go.mod h1:I/ZwlWM0sbFLhbStpDOeimjtMbWpMFSoGdVmzYxLGDg= go.opentelemetry.io/collector/consumer/consumertest v0.120.0 h1:iPFmXygDsDOjqwdQ6YZcTmpiJeQDJX+nHvrjTPsUuv4= @@ -1982,9 +3362,13 @@ go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29 h1:w0QrHuh0hhUZ++UTQaBM2DMdrWQghZ/UsUb+Wb1+8YE= go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= @@ -1997,6 +3381,10 @@ go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 h1:X66ZEoMN2SuaoI/dfZVYobB6E5zjZyyHUMWlCA7MgGE= go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y= +gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng= +gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ= +golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= +golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -2031,6 +3419,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= +golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -2057,8 +3447,10 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= @@ -2299,6 +3691,8 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0= +golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2416,6 +3810,8 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= +golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2428,17 +3824,24 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= +golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= +gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -2508,8 +3911,8 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genai v0.7.0 h1:TINBYXnP+K+D8b16LfVyb6XR3kdtieXy6nJsGoEXcBc= -google.golang.org/genai v0.7.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= +google.golang.org/genai v1.10.0 h1:ETP0Yksn5KUSEn5+ihMOnP3IqjZ+7Z4i0LjJslEXatI= +google.golang.org/genai v1.10.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -2643,6 +4046,8 @@ google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2Z google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20250425173222-7b384671a197 h1:km2eaPKRd1ganBNsKcAd9YOCLvTBQe8iTaPhE2q9n24= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20250425173222-7b384671a197/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 h1:29cjnHVylHwTzH66WfFZqgSQgnxzvWE+jvBwpZCLRxY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -2689,6 +4094,10 @@ google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpX google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -2716,13 +4125,31 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= +gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/jinzhu/gorm.v1 v1.9.2 h1:sTqyEcgrxG68jdeUXA9syQHNdeRhhfaYZ+vcL3x730I= +gopkg.in/jinzhu/gorm.v1 v1.9.2/go.mod h1:56JJPUzbikvTVnoyP1nppSkbJ2L8sunqTBDY2fDrmFg= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/olivere/elastic.v3 v3.0.75 h1:u3B8p1VlHF3yNLVOlhIWFT3F1ICcHfM5V6FFJe6pPSo= +gopkg.in/olivere/elastic.v3 v3.0.75/go.mod h1:yDEuSnrM51Pc8dM5ov7U8aI/ToR3PG0llA8aRv2qmw0= +gopkg.in/olivere/elastic.v5 v5.0.84 h1:acF/tRSg5geZpE3rqLglkS79CQMIMzOpWZE7hRXIkjs= +gopkg.in/olivere/elastic.v5 v5.0.84/go.mod h1:LXF6q9XNBxpMqrcgax95C6xyARXWbbCXUrtTxrNrxJI= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -2732,12 +4159,22 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw= +gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw= +gorm.io/driver/postgres v1.4.6 h1:1FPESNXqIKG5JmraaH2bfCVlMQ7paLoCreFxDtqzwdc= +gorm.io/driver/postgres v1.4.6/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4= +gorm.io/driver/sqlserver v1.4.2 h1:nMtEeKqv2R/vv9FoHUFWfXfP6SskAgRar0TPlZV1stk= +gorm.io/driver/sqlserver v1.4.2/go.mod h1:XHwBuB4Tlh7DqO0x7Ema8dmyWsQW7wi38VQOAFkrbXY= +gorm.io/gorm v1.25.3 h1:zi4rHZj1anhZS2EuEODMhDisGy+Daq9jtPrNGgbQYD8= +gorm.io/gorm v1.25.3/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc h1:DXLLFYv/k/xr0rWcwVEvWme1GR36Oc4kNMspg38JeiE= gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= +helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= +helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2746,10 +4183,30 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= +honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= +k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= +k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= +k8s.io/cli-runtime v0.33.0 h1:Lbl/pq/1o8BaIuyn+aVLdEPHVN665tBAXUePs8wjX7c= +k8s.io/cli-runtime v0.33.0/go.mod h1:QcA+r43HeUM9jXFJx7A+yiTPfCooau/iCcP1wQh4NFw= +k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= +k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= +k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kubectl v0.33.0 h1:HiRb1yqibBSCqic4pRZP+viiOBAnIdwYDpzUFejs07g= +k8s.io/kubectl v0.33.0/go.mod h1:gAlGBuS1Jq1fYZ9AjGWbI/5Vk3M/VW2DK4g10Fpyn/0= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= kernel.org/pub/linux/libs/security/libcap/cap v1.2.73 h1:Th2b8jljYqkyZKS3aD3N9VpYsQpHuXLgea+SZUIfODA= @@ -2758,17 +4215,39 @@ kernel.org/pub/linux/libs/security/libcap/psx v1.2.73 h1:SEAEUiPVylTD4vqqi+vtGkS kernel.org/pub/linux/libs/security/libcap/psx v1.2.73/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= +lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= +mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= +modernc.org/b v1.0.0 h1:vpvqeyp17ddcQWF29Czawql4lDdABCDRbXRAS4+aF2o= +modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= +modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0= +modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/db v1.0.0 h1:2c6NdCfaLnshSvY7OU09cyAY0gYXUZj4lmg5ItHyucg= +modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= +modernc.org/file v1.0.0 h1:9/PdvjVxd5+LcWUQIfapAWRGOkDLK90rloa8s/au06A= +modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= +modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/golex v1.0.0 h1:wWpDlbK8ejRfSyi0frMyhilD3JBvtcx2AdGDnU+JtsE= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/internal v1.0.0 h1:XMDsFDcBDsibbBnHB2xzljZ+B1yrOVLEFkKL2u15Glw= +modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= @@ -2778,6 +4257,8 @@ modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/lldb v1.0.0 h1:6vjDJxQEfhlOLwl4bhpwIz00uyFK4EmSYcbwqwbynsc= +modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= @@ -2789,24 +4270,73 @@ modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/ql v1.0.0 h1:bIQ/trWNVjQPlinI6jdOQsi195SIturGo3mp5hsDqVU= +modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= +modernc.org/sortutil v1.1.0 h1:oP3U4uM+NT/qBQcbg/K2iqAX0Nx7B1b6YZtq3Gk/PjM= +modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +modernc.org/zappy v1.0.0 h1:dPVaP+3ueIUv4guk8PuZ2wiUGcJ1WUVvIheeSSTD0yk= +modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= +mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= +mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw= +mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg= +mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 h1:VuJo4Mt0EVPychre4fNlDWDuE5AjXtPJpRUWqZDQhaI= +mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8/go.mod h1:Oh/d7dEtzsNHGOq1Cdv8aMm3KdKhVvPbRQcM8WFpBR8= +oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= +oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= +pack.ag/tftp v1.0.1-0.20181129014014-07909dfbde3c h1:4DHuGX0VtxRIyjXlVpcjSGEmZ7OnIK7Hvo+INnxI8yk= +pack.ag/tftp v1.0.1-0.20181129014014-07909dfbde3c/go.mod h1:N1Pyo5YG+K90XHoR2vfLPhpRuE8ziqbgMn/r/SghZas= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU= +sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kind v0.19.0 h1:ZSUh6/kpab6fiowT6EqL4k8xSbedI2NWxyuUOtoPFe4= +sigs.k8s.io/kind v0.19.0/go.mod h1:aBlbxg08cauDgZ612shr017/rZwqd7AS563FvpWKPVs= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= +src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f h1:pjVeIo9Ba6K1Wy+rlwX91zT7A+xGEmxiNRBdN04gDTQ= +src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f/go.mod h1:kPbhv5+fBeUh85nET3wWhHGUaUQ64nZMJ8FwA5v5Olg= storj.io/drpc v0.0.33 h1:yCGZ26r66ZdMP0IcTYsj7WDAUIIjzXk6DJhbhvt9FHI= storj.io/drpc v0.0.33/go.mod h1:vR804UNzhBa49NOJ6HeLjd2H3MakC1j5Gv8bsOQT6N4= diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 9fa6e45fa30da..8101055f2fe3e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -317,11 +317,11 @@ export interface ChatMessage { readonly createdAt?: Record; readonly content: string; readonly role: string; - // external type "github.com/kylecarbs/aisdk-go.Part", to include this type the package must be explicitly included in the parsing + // external type "github.com/coder/aisdk-go.Part", to include this type the package must be explicitly included in the parsing readonly parts?: readonly unknown[]; // empty interface{} type, falling back to unknown readonly annotations?: readonly unknown[]; - // external type "github.com/kylecarbs/aisdk-go.Attachment", to include this type the package must be explicitly included in the parsing + // external type "github.com/coder/aisdk-go.Attachment", to include this type the package must be explicitly included in the parsing readonly experimental_attachments?: readonly unknown[]; } @@ -349,7 +349,7 @@ export interface ConvertLoginRequest { // From codersdk/chat.go export interface CreateChatMessageRequest { readonly model: string; - // external type "github.com/kylecarbs/aisdk-go.Message", to include this type the package must be explicitly included in the parsing + // external type "github.com/coder/aisdk-go.Message", to include this type the package must be explicitly included in the parsing readonly message: unknown; readonly thinking: boolean; } From e1c2e004464a1c891416843d9d8959a0cddd3635 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 9 Jun 2025 11:02:29 +0200 Subject: [PATCH 08/61] chore: get openai proxy working Signed-off-by: Danny Kopping --- aibridged/bridge.go | 115 +++- go.sum | 1530 ------------------------------------------- 2 files changed, 98 insertions(+), 1547 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index fe316f8c9ef62..c688cb4ce3844 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -16,9 +16,10 @@ import ( "time" "github.com/anthropics/anthropic-sdk-go" - "github.com/anthropics/anthropic-sdk-go/packages/ssestream" + ant_ssestream "github.com/anthropics/anthropic-sdk-go/packages/ssestream" "github.com/charmbracelet/log" "github.com/openai/openai-go" + openai_ssestream "github.com/openai/openai-go/packages/ssestream" "golang.org/x/xerrors" "github.com/coder/coder/v2/aibridged/proto" @@ -61,24 +62,13 @@ func NewBridge(addr string, clientFn func() (proto.DRPCAIBridgeDaemonClient, boo } func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - if err != nil { - // TODO: error handling. - panic(err) - return - } - r.Body.Close() - - var msg openai.ChatCompletionNewParams - err = json.Unmarshal(body, &msg) - if err != nil { - // TODO: error handling. - panic(err) + coderdClient, ok := b.clientFn() + if !ok { + // TODO: log issue. + http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) return } - fmt.Println(msg) - target, err := url.Parse("https://api.openai.com") if err != nil { http.Error(w, "failed to parse OpenAI URL", http.StatusInternalServerError) @@ -103,8 +93,99 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { req.URL.Scheme = target.Scheme req.URL.Host = target.Host + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, "could not ready request body", http.StatusBadRequest) + return + } + _ = req.Body.Close() + + var msg openai.ChatCompletionNewParams + err = json.NewDecoder(bytes.NewReader(body)).Decode(&msg) + if err != nil { + http.Error(w, "could not unmarshal request body", http.StatusBadRequest) + return + } + + // TODO: robustness + if len(msg.Messages) > 0 { + latest := msg.Messages[len(msg.Messages)-1] + if latest.OfUser != nil { + if latest.OfUser.Content.OfString.String() != "" { + _, _ = coderdClient.TrackUserPrompts(r.Context(), &proto.TrackUserPromptsRequest{ + Prompt: strings.TrimSpace(latest.OfUser.Content.OfString.String()), + }) + } else { + fmt.Println() + } + } + } + + req.Body = io.NopCloser(bytes.NewReader(body)) + fmt.Printf("Proxying %s request to: %s\n", req.Method, req.URL.String()) } + proxy.ModifyResponse = func(response *http.Response) error { + body, err := io.ReadAll(response.Body) + if err != nil { + return xerrors.Errorf("read response body: %w", err) + } + if err = response.Body.Close(); err != nil { + return xerrors.Errorf("close body: %w", err) + } + + if !strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { + var msg openai.ChatCompletion + + // TODO: check content-encoding to handle others. + gr, err := gzip.NewReader(bytes.NewReader(body)) + if err != nil { + return xerrors.Errorf("parse gzip-encoded body: %w", err) + } + + err = json.NewDecoder(gr).Decode(&msg) + if err != nil { + return xerrors.Errorf("parse non-streaming body: %w", err) + } + + _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ + MsgId: msg.ID, + InputTokens: msg.Usage.PromptTokens, + OutputTokens: msg.Usage.CompletionTokens, + }) + + response.Body = io.NopCloser(bytes.NewReader(body)) + return nil + } + + response.Body = io.NopCloser(bytes.NewReader(body)) + stream := openai_ssestream.NewStream[openai.ChatCompletionChunk](openai_ssestream.NewDecoder(response), nil) + + var ( + inputToks, outputToks int64 + ) + + var msg openai.ChatCompletionAccumulator + for stream.Next() { + chunk := stream.Current() + msg.AddChunk(chunk) + + if msg.Usage.PromptTokens+msg.Usage.CompletionTokens > 0 { + inputToks = msg.Usage.PromptTokens + outputToks = msg.Usage.CompletionTokens + } + } + + _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ + MsgId: msg.ID, + InputTokens: inputToks, + OutputTokens: outputToks, + }) + + response.Body = io.NopCloser(bytes.NewReader(body)) + + return nil + } proxy.ServeHTTP(w, r) } @@ -207,7 +288,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } response.Body = io.NopCloser(bytes.NewReader(body)) - stream := ssestream.NewStream[anthropic.MessageStreamEventUnion](ssestream.NewDecoder(response), nil) + stream := ant_ssestream.NewStream[anthropic.MessageStreamEventUnion](ant_ssestream.NewDecoder(response), nil) var ( inputToks, outputToks int64 diff --git a/go.sum b/go.sum index 8a026e07348fa..364f2a1fe9bac 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,3 @@ -4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA= -4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= -4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= -4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb h1:4MKA8lBQLnCqj2myJCb5Lzoa65y0tABO4gHrxuMdsCQ= cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ= cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= @@ -47,61 +43,42 @@ cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit6 cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= -cloud.google.com/go/accessapproval v1.8.3 h1:axlU03FRiXDNupsmPG7LKzuS4Enk1gf598M62lWVB74= -cloud.google.com/go/accessapproval v1.8.3/go.mod h1:3speETyAv63TDrDmo5lIkpVueFkQcQchkiw/TAMbBo4= cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= -cloud.google.com/go/accesscontextmanager v1.9.3 h1:8zVoeiBa4erMCLEXltOcqVEsZhS26JZ5/Vrgs59eQiI= -cloud.google.com/go/accesscontextmanager v1.9.3/go.mod h1:S1MEQV5YjkAKBoMekpGrkXKfrBdsi4x6Dybfq6gZ8BU= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= -cloud.google.com/go/aiplatform v1.74.0 h1:rE2P5H7FOAFISAZilmdkapbk4CVgwfVs6FDWlhGfuy0= -cloud.google.com/go/aiplatform v1.74.0/go.mod h1:hVEw30CetNut5FrblYd1AJUWRVSIjoyIvp0EVUh51HA= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= -cloud.google.com/go/analytics v0.26.0 h1:O2kWr2Sd4ep3I+YJ4aiY0G4+zWz6sp4eTce+JVns9TM= -cloud.google.com/go/analytics v0.26.0/go.mod h1:KZWJfs8uX/+lTjdIjvT58SFa86V9KM6aPXwZKK6uNVI= cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= -cloud.google.com/go/apigateway v1.7.3 h1:Mn7cC5iWJz+cSMS/Hb+N2410CpZ6c8XpJKaexBl0Gxs= -cloud.google.com/go/apigateway v1.7.3/go.mod h1:uK0iRHdl2rdTe79bHW/bTsKhhXPcFihjUdb7RzhTPf4= cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= -cloud.google.com/go/apigeeconnect v1.7.3 h1:Wlr+30Tha0SMCvQYZKdrh+HkpOyl0CQFSlzeY/Gg1gs= -cloud.google.com/go/apigeeconnect v1.7.3/go.mod h1:2ZkT5VCAqhYrDqf4dz7lGp4N/+LeNBSfou8Qs5bIuSg= cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= -cloud.google.com/go/apigeeregistry v0.9.3 h1:j9CJg/oC884OX5cDpiwNt1ZlDXNV6Zb9Mp1YmRrOG0k= -cloud.google.com/go/apigeeregistry v0.9.3/go.mod h1:oNCP2VjOeI6U8yuOuTmU4pkffdcXzR5KxeUD71gF+Dg= cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= -cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= -cloud.google.com/go/appengine v1.9.3 h1:jrcanSzj9J1erevZuxldvsDwY+0k/DeFFzlnSfPGfL8= -cloud.google.com/go/appengine v1.9.3/go.mod h1:DtLsE/z3JufM/pCEIyVYebJ0h9UNPpN64GZQrYgOSyM= cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= -cloud.google.com/go/area120 v0.9.3 h1:dPQ07rW4eku8OgNWDOaQaVGcE4+XfhH8BSbVwdVQ+wU= -cloud.google.com/go/area120 v0.9.3/go.mod h1:F3vxS/+hqzrjJo55Xvda3Jznjjbd+4Foo43SN5eMd8M= cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= @@ -110,8 +87,6 @@ cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1 cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= -cloud.google.com/go/artifactregistry v1.16.1 h1:ZNXGB6+T7VmWdf6//VqxLdZ/sk0no8W0ujanHeJwDRw= -cloud.google.com/go/artifactregistry v1.16.1/go.mod h1:sPvFPZhfMavpiongKwfg93EOwJ18Tnj9DIwTU9xWUgs= cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= @@ -120,16 +95,12 @@ cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAt cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= -cloud.google.com/go/asset v1.20.4 h1:6oNgjcs5KCPGBD71G0IccK6TfeFsEtBTyQ3Q+Dn09bs= -cloud.google.com/go/asset v1.20.4/go.mod h1:DP09pZ+SoFWUZyPZx26xVroHk+6+9umnQv+01yfJxbM= cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/assuredworkloads v1.12.3 h1:RU1WhF1zMggdXAZ+ezYTn4Eh/FdiX7sz8lLXGERn4Po= -cloud.google.com/go/assuredworkloads v1.12.3/go.mod h1:iGBkyMGdtlsxhCi4Ys5SeuvIrPTeI6HeuEJt7qJgJT8= cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -139,24 +110,16 @@ cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/I cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= -cloud.google.com/go/automl v1.14.4 h1:vkD+hQ75SMINMgJBT/KDpFYvfQLzJbtIQZdw0AWq8Rs= -cloud.google.com/go/automl v1.14.4/go.mod h1:sVfsJ+g46y7QiQXpVs9nZ/h8ntdujHm5xhjHW32b3n4= cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= -cloud.google.com/go/baremetalsolution v1.3.3 h1:OL+KT+wCumdDhG44aeqGAdkwdT8Wa4Lh+o4INM+CQjw= -cloud.google.com/go/baremetalsolution v1.3.3/go.mod h1:uF9g08RfmXTF6ZKbXxixy5cGMGFcG6137Z99XjxLOUI= cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= -cloud.google.com/go/batch v1.12.0 h1:lXuTaELvU0P0ARbTFxxdpOC/dFnZZeGglSw06BtO//8= -cloud.google.com/go/batch v1.12.0/go.mod h1:CATSBh/JglNv+tEU/x21Z47zNatLQ/gpGnpyKOzbbcM= cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= -cloud.google.com/go/beyondcorp v1.1.3 h1:ezavJc0Gzh4N8zBskO/DnUVMWPa8lqH/tmQSyaknmCA= -cloud.google.com/go/beyondcorp v1.1.3/go.mod h1:3SlVKnlczNTSQFuH5SSyLuRd4KaBSc8FH/911TuF/Cc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -170,56 +133,38 @@ cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/Zur cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= -cloud.google.com/go/bigquery v1.66.2 h1:EKOSqjtO7jPpJoEzDmRctGea3c2EOGoexy8VyY9dNro= -cloud.google.com/go/bigquery v1.66.2/go.mod h1:+Yd6dRyW8D/FYEjUGodIbu0QaoEmgav7Lwhotup6njo= -cloud.google.com/go/bigtable v1.35.0 h1:UEacPwaejN2mNbz67i1Iy3G812rxtgcs6ePj1TAg7dw= -cloud.google.com/go/bigtable v1.35.0/go.mod h1:EabtwwmTcOJFXp+oMZAT/jZkyDIjNwrv53TrS4DGrrM= cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= -cloud.google.com/go/billing v1.20.1 h1:xMlO3hc5BI0s23tRB40bL40xSpxUR1x3E07Y5/VWcjU= -cloud.google.com/go/billing v1.20.1/go.mod h1:DhT80hUZ9gz5UqaxtK/LNoDELfxH73704VTce+JZqrY= cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= -cloud.google.com/go/binaryauthorization v1.9.3 h1:X8JRfmk0/vyRqLusEyAPr0nZCK6RKae9omB4lrit0XI= -cloud.google.com/go/binaryauthorization v1.9.3/go.mod h1:f3xcb/7vWklDoF+q2EaAIS+/A/e1278IgiYxonRX+Jk= cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= -cloud.google.com/go/certificatemanager v1.9.3 h1:2UP31fg7b+y3F0OmNbPHOKPEJ+6LOMfxAXX4p8xGCy4= -cloud.google.com/go/certificatemanager v1.9.3/go.mod h1:O5T4Lg/dHbDHLFFooV2Mh/VsT3Mj2CzPEWRo4qw5prc= cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= -cloud.google.com/go/channel v1.19.2 h1:oHyO3QAZ6kdf6SwqnUTBz50ND6Nk2rxZtboUiF4dgLE= -cloud.google.com/go/channel v1.19.2/go.mod h1:syX5opXGXFt17DHCyCdbdlM464Tx0gHMi46UlEWY9Gg= cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= -cloud.google.com/go/cloudbuild v1.22.0 h1:zmDznviZpvkCla0adbp7jJsMYZ9bABCbcPK2cBUHwg8= -cloud.google.com/go/cloudbuild v1.22.0/go.mod h1:p99MbQrzcENHb/MqU3R6rpqFRk/X+lNG3PdZEIhM95Y= cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= -cloud.google.com/go/clouddms v1.8.4 h1:CDOd1nwmP4uek+nZhl4bhRIpzj8jMqoMRqKAfKlgLhw= -cloud.google.com/go/clouddms v1.8.4/go.mod h1:RadeJ3KozRwy4K/gAs7W74ZU3GmGgVq5K8sRqNs3HfA= cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= -cloud.google.com/go/cloudtasks v1.13.3 h1:rXdznKjCa7WpzmvR2plrn2KJ+RZC1oYxPiRWNQjjf3k= -cloud.google.com/go/cloudtasks v1.13.3/go.mod h1:f9XRvmuFTm3VhIKzkzLCPyINSU3rjjvFUsFVGR5wi24= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= @@ -235,8 +180,6 @@ cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63 cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= -cloud.google.com/go/compute v1.34.0 h1:+k/kmViu4TEi97NGaxAATYtpYBviOWJySPZ+ekA95kk= -cloud.google.com/go/compute v1.34.0/go.mod h1:zWZwtLwZQyonEvIQBuIa0WvraMYK69J5eDCOw9VZU4g= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= @@ -246,21 +189,15 @@ cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQ cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= -cloud.google.com/go/contactcenterinsights v1.17.1 h1:xJoZbX0HM1zht8KxAB38hs2v4Hcl+vXGLo454LrdwxA= -cloud.google.com/go/contactcenterinsights v1.17.1/go.mod h1:n8OiNv7buLA2AkGVkfuvtW3HU13AdTmEwAlAu46bfxY= cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= -cloud.google.com/go/container v1.42.2 h1:8ncSEBjkng6ucCICauaUGzBomoM2VyYzleAum1OFcow= -cloud.google.com/go/container v1.42.2/go.mod h1:y71YW7uR5Ck+9Vsbst0AF2F3UMgqmsN4SP8JR9xEsR8= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= -cloud.google.com/go/containeranalysis v0.13.3 h1:1D8U75BeotZxrG4jR6NYBtOt+uAeBsWhpBZmSYLakQw= -cloud.google.com/go/containeranalysis v0.13.3/go.mod h1:0SYnagA1Ivb7qPqKNYPkCtphhkJn3IzgaSp3mj+9XAY= cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= @@ -269,67 +206,44 @@ cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOX cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= -cloud.google.com/go/datacatalog v1.24.3 h1:3bAfstDB6rlHyK0TvqxEwaeOvoN9UgCs2bn03+VXmss= -cloud.google.com/go/datacatalog v1.24.3/go.mod h1:Z4g33XblDxWGHngDzcpfeOU0b1ERlDPTuQoYG6NkF1s= cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= -cloud.google.com/go/dataflow v0.10.3 h1:+7IfIXzYWSybIIDGK9FN2uqBsP/5b/Y0pBYzNhcmKSU= -cloud.google.com/go/dataflow v0.10.3/go.mod h1:5EuVGDh5Tg4mDePWXMMGAG6QYAQhLNyzxdNQ0A1FfW4= cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= -cloud.google.com/go/dataform v0.10.3 h1:ZpGkZV8OyhUhvN/tfLffU2ki5ERTtqOunkIaiVAhmw0= -cloud.google.com/go/dataform v0.10.3/go.mod h1:8SruzxHYCxtvG53gXqDZvZCx12BlsUchuV/JQFtyTCw= cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= -cloud.google.com/go/datafusion v1.8.3 h1:FTMtsf2nfGGlDCuE84/RvVaCcTIYE7WQSB0noeO0cwI= -cloud.google.com/go/datafusion v1.8.3/go.mod h1:hyglMzE57KRf0Rf/N2VRPcHCwKfZAAucx+LATY6Jc6Q= cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= -cloud.google.com/go/datalabeling v0.9.3 h1:PqoA3gnOWaLcHCnqoZe4jh3jmiv6+Z7W2xUUkw/j4jE= -cloud.google.com/go/datalabeling v0.9.3/go.mod h1:3LDFUgOx+EuNUzDyjU7VElO8L+b5LeaZEFA/ZU1O1XU= cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= -cloud.google.com/go/dataplex v1.22.0 h1:j4hD6opb+gq9CJNPFIlIggoW8Kjymg8Wmy2mdHmQoiw= -cloud.google.com/go/dataplex v1.22.0/go.mod h1:g166QMCGHvwc3qlTG4p34n+lHwu7JFfaNpMfI2uO7b8= cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= -cloud.google.com/go/dataproc/v2 v2.11.0 h1:6aRpyoRfNOP+r2+pGb7HeHtF+SYQID8kzztfHuK0plk= -cloud.google.com/go/dataproc/v2 v2.11.0/go.mod h1:9vgGrn57ra7KBqz+B2KD+ltzEXvnHAUClFgq/ryU99g= cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= -cloud.google.com/go/dataqna v0.9.3 h1:lGUj2FYs650EUPDMV6plWBAoh8qH9Bu1KCz1PUYF2VY= -cloud.google.com/go/dataqna v0.9.3/go.mod h1:PiAfkXxa2LZYxMnOWVYWz3KgY7txdFg9HEMQPb4u1JA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= -cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= -cloud.google.com/go/datastream v1.13.0 h1:C5AeEdze55feJVb17a40QmlnyH/aMhn/uf3Go3hIqPA= -cloud.google.com/go/datastream v1.13.0/go.mod h1:GrL2+KC8mV4GjbVG43Syo5yyDXp3EH+t6N2HnZb1GOQ= cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= -cloud.google.com/go/deploy v1.26.2 h1:1c2Cd3jdb0mrKHHfyzSQ5DRmxgYd07tIZZzuMNrwDxU= -cloud.google.com/go/deploy v1.26.2/go.mod h1:XpS3sG/ivkXCfzbzJXY9DXTeCJ5r68gIyeOgVGxGNEs= cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= @@ -338,55 +252,35 @@ cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFM cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= -cloud.google.com/go/dialogflow v1.66.0 h1:/kfpZw20/3v4sC8czEIuvn3Bu3qOne5aHDYlRYHbu18= -cloud.google.com/go/dialogflow v1.66.0/go.mod h1:BPiRTnnXP/tHLot5h/U62Xcp+i6ekRj/bq6uq88p+Lw= cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= -cloud.google.com/go/dlp v1.21.0 h1:9kz7+gaB/0gBZsDUnNT1asDihNZSrRFSeUTBcBdUAkk= -cloud.google.com/go/dlp v1.21.0/go.mod h1:Y9HOVtPoArpL9sI1O33aN/vK9QRwDERU9PEJJfM8DvE= cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= -cloud.google.com/go/documentai v1.35.2 h1:hswVobCWUTXtmn+4QqUIVkai7sDOe0QS2KB3IpqLkik= -cloud.google.com/go/documentai v1.35.2/go.mod h1:oh/0YXosgEq3hVhyH4ZQ7VNXPaveRO4eLVM3tBSZOsI= cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= -cloud.google.com/go/domains v0.10.3 h1:wnqN5YwMrtLSjn+HB2sChgmZ6iocOta4Q41giQsiRjY= -cloud.google.com/go/domains v0.10.3/go.mod h1:m7sLe18p0PQab56bVH3JATYOJqyRHhmbye6gz7isC7o= cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= -cloud.google.com/go/edgecontainer v1.4.1 h1:SwQuHQiheVfL7b5ar/AXDberiaqr/yiue8X55AdWnZU= -cloud.google.com/go/edgecontainer v1.4.1/go.mod h1:ubMQvXSxsvtEjJLyqcPFrdWrHfvjQxdoyt+SUrAi5ek= cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/errorreporting v0.3.2 h1:isaoPwWX8kbAOea4qahcmttoS79+gQhvKsfg5L5AgH8= -cloud.google.com/go/errorreporting v0.3.2/go.mod h1:s5kjs5r3l6A8UUyIsgvAhGq6tkqyBCUss0FRpsoVTww= cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= -cloud.google.com/go/essentialcontacts v1.7.3 h1:Paw495vxVyKuAgcQ2NQk09iRZBhPYRytknydEnvzcv4= -cloud.google.com/go/essentialcontacts v1.7.3/go.mod h1:uimfZgDbhWNCmBpwUUPHe4vcMY2azsq/axC9f7vZFKI= cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= -cloud.google.com/go/eventarc v1.15.1 h1:RMymT7R87LaxKugOKwooOoheWXUm1NMeOfh3CVU9g54= -cloud.google.com/go/eventarc v1.15.1/go.mod h1:K2luolBpwaVOujZQyx6wdG4n2Xum4t0q1cMBmY1xVyI= cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= -cloud.google.com/go/filestore v1.9.3 h1:vTXQI5qYKZ8dmCyHN+zVfaMyXCYbyZNM0CkPzpPUn7Q= -cloud.google.com/go/filestore v1.9.3/go.mod h1:Me0ZRT5JngT/aZPIKpIK6N4JGMzrFHRtGHd9ayUS4R4= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= -cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= @@ -394,42 +288,28 @@ cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5Uwt cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= -cloud.google.com/go/functions v1.19.3 h1:V0vCHSgFTUqKn57+PUXp1UfQY0/aMkveAw7wXeM3Lq0= -cloud.google.com/go/functions v1.19.3/go.mod h1:nOZ34tGWMmwfiSJjoH/16+Ko5106x+1Iji29wzrBeOo= cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc= cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= -cloud.google.com/go/gkebackup v1.6.3 h1:djdExe/QgoKdp1gnIO1G5BoO1o/yGQOQJJEZ4QKTEXQ= -cloud.google.com/go/gkebackup v1.6.3/go.mod h1:JJzGsA8/suXpTDtqI7n9RZW97PXa2CIp+n8aRC/y57k= cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= -cloud.google.com/go/gkeconnect v0.12.1 h1:YVpR0vlHSP/wD74PXEbKua4Aamud+wiYm4TiewNjD3M= -cloud.google.com/go/gkeconnect v0.12.1/go.mod h1:L1dhGY8LjINmWfR30vneozonQKRSIi5DWGIHjOqo58A= cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= -cloud.google.com/go/gkehub v0.15.3 h1:yZ6lNJ9rNIoQmWrG14dB3+BFjS/EIRBf7Bo6jc5QWlE= -cloud.google.com/go/gkehub v0.15.3/go.mod h1:nzFT/Q+4HdQES/F+FP1QACEEWR9Hd+Sh00qgiH636cU= cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= -cloud.google.com/go/gkemulticloud v1.5.1 h1:JWe6PDNpNU88ZYvQkTd7w28fgeIs/gg6i0hcjUkgZ3M= -cloud.google.com/go/gkemulticloud v1.5.1/go.mod h1:OdmhfSPXuJ0Kn9dQ2I3Ou7XZ3QK8caV4XVOJZwrIa3s= -cloud.google.com/go/grafeas v0.2.0 h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdELM= cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= -cloud.google.com/go/gsuiteaddons v1.7.4 h1:f3eMYsCDdg2AeldIPdKmBRxN1WoiTpE3RvX5orcm/I8= -cloud.google.com/go/gsuiteaddons v1.7.4/go.mod h1:gpE2RUok+HUhuK7RPE/fCOEgnTffS0lCHRaAZLxAMeE= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= @@ -446,19 +326,13 @@ cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/ cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= -cloud.google.com/go/iap v1.10.3 h1:OWNYFHPyIBNHEAEFdVKOltYWe0g3izSrpFJW6Iidovk= -cloud.google.com/go/iap v1.10.3/go.mod h1:xKgn7bocMuCFYhzRizRWP635E2LNPnIXT7DW0TlyPJ8= cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= -cloud.google.com/go/ids v1.5.3 h1:wbFF7twu0XScFr+dtsVxTTttbFIRYt/SJjZiHFidtYE= -cloud.google.com/go/ids v1.5.3/go.mod h1:a2MX8g18Eqs7yxD/pnEdid42SyBUm9LIzSWf8Jux9OY= cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= -cloud.google.com/go/iot v1.8.3 h1:aPWYQ+A1NX6ou/5U0nFAiXWdVT8OBxZYVZt2fBl2gWA= -cloud.google.com/go/iot v1.8.3/go.mod h1:dYhrZh+vUxIQ9m3uajyKRSW7moF/n0rYmA2PhYAkMFE= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= @@ -466,20 +340,14 @@ cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4 cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/kms v1.21.0 h1:x3EeWKuYwdlo2HLse/876ZrKjk2L5r7Uexfm8+p6mSI= -cloud.google.com/go/kms v1.21.0/go.mod h1:zoFXMhVVK7lQ3JC9xmhHMoQhnjEDZFoLAr5YMwzBLtk= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= -cloud.google.com/go/language v1.14.3 h1:8hmFMiS3wjjj3TX/U1zZYTgzwZoUjDbo9PaqcYEmuB4= -cloud.google.com/go/language v1.14.3/go.mod h1:hjamj+KH//QzF561ZuU2J+82DdMlFUjmiGVWpovGGSA= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= -cloud.google.com/go/lifesciences v0.10.3 h1:Z05C+Ui953f0EQx9hJ1la6+QQl8ADrIs3iNwP5Elkpg= -cloud.google.com/go/lifesciences v0.10.3/go.mod h1:hnUUFht+KcZcliixAg+iOh88FUwAzDQQt5tWd7iIpNg= cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= @@ -492,32 +360,22 @@ cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8 cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= -cloud.google.com/go/managedidentities v1.7.3 h1:b9xGs24BIjfyvLgCtJoClOZpPi8d8owPgWe5JEINgaY= -cloud.google.com/go/managedidentities v1.7.3/go.mod h1:H9hO2aMkjlpY+CNnKWRh+WoQiUIDO8457wWzUGsdtLA= cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= -cloud.google.com/go/maps v1.19.0 h1:deVm1ZFyCrUwxG11CdvtBz350VG5JUQ/LHTLnQrBgrM= -cloud.google.com/go/maps v1.19.0/go.mod h1:goHUXrmzoZvQjUVd0KGhH8t3AYRm17P8b+fsyR1UAmQ= cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= -cloud.google.com/go/mediatranslation v0.9.3 h1:nRBjeaMLipw05Br+qDAlSCcCQAAlat4mvpafztbEVgc= -cloud.google.com/go/mediatranslation v0.9.3/go.mod h1:KTrFV0dh7duYKDjmuzjM++2Wn6yw/I5sjZQVV5k3BAA= cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= -cloud.google.com/go/memcache v1.11.3 h1:XH/qT3GbbSH//R0JTqR77lRpBxaa0N9sHgAzfwbTrv0= -cloud.google.com/go/memcache v1.11.3/go.mod h1:UeWI9cmY7hvjU1EU6dwJcQb6EFG4GaM3KNXOO2OFsbI= cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= -cloud.google.com/go/metastore v1.14.3 h1:jDqeCw6NGDRAPT9+2Y/EjnWAB0BfCcUfmPLOyhB0eHs= -cloud.google.com/go/metastore v1.14.3/go.mod h1:HlbGVOvg0ubBLVFRk3Otj3gtuzInuzO/TImOBwsKlG4= cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= @@ -530,73 +388,49 @@ cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uG cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= -cloud.google.com/go/networkconnectivity v1.16.1 h1:YsVhG71ZC4FkqCP2oCI55x/JeGFyd7738Lt8iNTrzJw= -cloud.google.com/go/networkconnectivity v1.16.1/go.mod h1:GBC1iOLkblcnhcnfRV92j4KzqGBrEI6tT7LP52nZCTk= cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= -cloud.google.com/go/networkmanagement v1.18.0 h1:oEoFGPYxTBsY47h0zdoE2ojV5aU/541D83UmxfjHWaE= -cloud.google.com/go/networkmanagement v1.18.0/go.mod h1:yTxpAFuvQOOKgL3W7+k2Rp1bSKTxyRcZ5xNHGdHUM6w= cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= -cloud.google.com/go/networksecurity v0.10.3 h1:JLJBFbxc8D7/OS81MyRoKhc2OvnVJxy5VMoQqqAhA7k= -cloud.google.com/go/networksecurity v0.10.3/go.mod h1:G85ABVcPscEgpw+gcu+HUxNZJWjn3yhTqEU7+SsltFM= cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= -cloud.google.com/go/notebooks v1.12.3 h1:+9DrGJcZhCu6B2t0JJorekjIUBvg/KvBmXJYGmfvVvA= -cloud.google.com/go/notebooks v1.12.3/go.mod h1:I0pMxZct+8Rega2LYrXL8jGAGZgLchSmh8Ksc+0xNyA= cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= -cloud.google.com/go/optimization v1.7.3 h1:JwQjjoBZJpsoMQe/3mhVBMVZuSdagHg2pGOnwh2Jk+E= -cloud.google.com/go/optimization v1.7.3/go.mod h1:GlYFp4Mju0ybK5FlOUtV6zvWC00TIScdbsPyF6Iv144= cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= -cloud.google.com/go/orchestration v1.11.4 h1:SFAsKyqvtS8VFcsq+JgXAeRkrksB9UH+AH7iFamkmlc= -cloud.google.com/go/orchestration v1.11.4/go.mod h1:UKR2JwogaZmDGnAcBgAQgCPn89QMqhXFUCYVhHd31vs= cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= -cloud.google.com/go/orgpolicy v1.14.2 h1:WFvgmjq/FO5GiXlhebltA9N14KdbLMcgG88ME+SWeBo= -cloud.google.com/go/orgpolicy v1.14.2/go.mod h1:2fTDMT3X048iFKxc6DEgkG+a/gN+68qEgtPrHItKMzo= cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= -cloud.google.com/go/osconfig v1.14.3 h1:cyf1PMK5c2/WOIr5r2lxjH/XBJMA9P4zC8Tm10i0z3M= -cloud.google.com/go/osconfig v1.14.3/go.mod h1:9D2MS1Etne18r/mAeW5jtto3toc9H1qu9wLNDG3NvQg= cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= -cloud.google.com/go/oslogin v1.14.3 h1:yomxnFPk+ye0zd0mJ15nn9fH4Ns7ex4xA3ll+u2q59A= -cloud.google.com/go/oslogin v1.14.3/go.mod h1:fDEGODTG/W9ZGUTHTlMh8euXWC1fTcgjJ9Kcxxy14a8= cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= -cloud.google.com/go/phishingprotection v0.9.3 h1:T5mGFV0ggBKg3qt9myFRiGJu+nIUucuHLAtVpAuQ08I= -cloud.google.com/go/phishingprotection v0.9.3/go.mod h1:ylzN9HruB/X7dD50I4sk+FfYzuPx9fm5JWsYI0t7ncc= cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= -cloud.google.com/go/policytroubleshooter v1.11.3 h1:ekIWI8JbKkpOfrgH/THGamQE/D16tcVBYJyrkseVcYI= -cloud.google.com/go/policytroubleshooter v1.11.3/go.mod h1:AFHlORqh4AnMC0twc2yPKfzlozp3DO0yo9OfOd9aNOs= cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= -cloud.google.com/go/privatecatalog v0.10.4 h1:fu2LABMi7CgZORQ2oNGbc0hoZ0FTqLkjGqIgAV/Kc7U= -cloud.google.com/go/privatecatalog v0.10.4/go.mod h1:n/vXBT+Wq8B4nSRUJNDsmqla5BYjbVxOlHzS6PjiF+w= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -605,14 +439,9 @@ cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcd cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= -cloud.google.com/go/pubsub v1.47.0 h1:Ou2Qu4INnf7ykrFjGv2ntFOjVo8Nloh/+OffF4mUu9w= -cloud.google.com/go/pubsub v1.47.0/go.mod h1:LaENesmga+2u0nDtLkIOILskxsfvn/BXX9Ak1NFxOs8= cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= -cloud.google.com/go/pubsublite v1.8.2 h1:jLQozsEVr+c6tOU13vDugtnaBSUy/PD5zK6mhm+uF1Y= -cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= -cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= @@ -621,66 +450,46 @@ cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= -cloud.google.com/go/recaptchaenterprise/v2 v2.19.4 h1:T5YGzaXwTesHaPDNTAuU3neDwZEnfjce70zufPFUwno= -cloud.google.com/go/recaptchaenterprise/v2 v2.19.4/go.mod h1:WaglfocMJGkqZVdXY/FVB7OhoVRONPS4uXqtNn6HfX0= cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= -cloud.google.com/go/recommendationengine v0.9.3 h1:kBpcYPx4ys4lrDGKp4OhP2uy8h7UjlmLW/qoO5Xb2bY= -cloud.google.com/go/recommendationengine v0.9.3/go.mod h1:QRnX5aM7DCvtqtSs7I0zay5Zfq3fzxqnsPbZF7pa1G8= cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= -cloud.google.com/go/recommender v1.13.3 h1:dVlOjxsbjuhlwu4MIcyPWe09qVcDqc419iOjdPl5RHk= -cloud.google.com/go/recommender v1.13.3/go.mod h1:6yAmcfqJRKglZrVuTHsieTFEm4ai9JtY3nQzmX4TC0Q= cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= -cloud.google.com/go/redis v1.18.0 h1:xcu35SCyHSp+nKV6QNIklgkBKTH1qb0aLUXjl0mSR8I= -cloud.google.com/go/redis v1.18.0/go.mod h1:fJ8dEQJQ7DY+mJRMkSafxQCuc8nOyPUwo9tXJqjvNEY= cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= -cloud.google.com/go/resourcemanager v1.10.3 h1:SHOMw0kX0xWratC5Vb5VULBeWiGlPYAs82kiZqNtWpM= -cloud.google.com/go/resourcemanager v1.10.3/go.mod h1:JSQDy1JA3K7wtaFH23FBGld4dMtzqCoOpwY55XYR8gs= cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= -cloud.google.com/go/resourcesettings v1.8.3 h1:13HOFU7v4cEvIHXSAQbinF4wp2Baybbq7q9FMctg1Ek= -cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= -cloud.google.com/go/retail v1.19.2 h1:PT6CUlazIFIOLLJnV+bPBtiSH8iusKZ+FZRzZYFt2vk= -cloud.google.com/go/retail v1.19.2/go.mod h1:71tRFYAcR4MhrZ1YZzaJxr030LvaZiIcupH7bXfFBcY= cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= -cloud.google.com/go/run v1.9.0 h1:9WeTqeEcriXqRViXMNwczjFJjixOSBlSlk/fW3lfKPg= -cloud.google.com/go/run v1.9.0/go.mod h1:Dh0+mizUbtBOpPEzeXMM22t8qYQpyWpfmUiWQ0+94DU= cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= -cloud.google.com/go/scheduler v1.11.4 h1:ewVvigBnEnrr9Ih8CKnLVoB5IiULaWfYU5nEnnfVAto= -cloud.google.com/go/scheduler v1.11.4/go.mod h1:0ylvH3syJnRi8EDVo9ETHW/vzpITR/b+XNnoF+GPSz4= cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/secretmanager v1.14.5 h1:W++V0EL9iL6T2+ec24Dm++bIti0tI6Gx6sCosDBters= -cloud.google.com/go/secretmanager v1.14.5/go.mod h1:GXznZF3qqPZDGZQqETZwZqHw4R6KCaYVvcGiRBA+aqY= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= @@ -688,21 +497,16 @@ cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= -cloud.google.com/go/security v1.18.3 h1:ya9gfY1ign6Yy25VMMMgZ9xy7D/TczDB0ElXcyWmEVE= -cloud.google.com/go/security v1.18.3/go.mod h1:NmlSnEe7vzenMRoTLehUwa/ZTZHDQE59IPRevHcpCe4= cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= -cloud.google.com/go/securitycenter v1.36.0 h1:IdDiAa7gYtL7Gdx+wEaNHimudk3ZkEGNhdz9FuEuxWM= -cloud.google.com/go/securitycenter v1.36.0/go.mod h1:AErAQqIvrSrk8cpiItJG1+ATl7SD7vQ6lgTFy/Tcs4Q= cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= -cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= @@ -710,36 +514,26 @@ cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPj cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= -cloud.google.com/go/servicedirectory v1.12.3 h1:oFkCp6ti7fc7hzeROmOPQuPBHFqwyhcsv3Yrma28+uc= -cloud.google.com/go/servicedirectory v1.12.3/go.mod h1:dwTKSCYRD6IZMrqoBCIvZek+aOYK/6+jBzOGw8ks5aY= cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= -cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= -cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= -cloud.google.com/go/shell v1.8.3 h1:mjYgUsOtV3jl9xvDmcvlRRmA64deEPf52zOfuc68b/g= -cloud.google.com/go/shell v1.8.3/go.mod h1:OYcrgWF6JSp/uk76sNTtYFlMD0ho2+Cdzc7U3P/bF54= cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/spanner v1.76.1 h1:vYbVZuXfnFwvNcvH3lhI2PeUA+kHyqKmLC7mJWaC4Ok= -cloud.google.com/go/spanner v1.76.1/go.mod h1:YtwoE+zObKY7+ZeDCBtZ2ukM+1/iPaMfUM+KnTh/sx0= cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= -cloud.google.com/go/speech v1.26.0 h1:qvURtJs7BQzQhbxWxwai0pT79S8KLVKJ/4W8igVkt1Y= -cloud.google.com/go/speech v1.26.0/go.mod h1:78bqDV2SgwFlP/M4n3i3PwLthFq6ta7qmyG6lUV7UCA= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -756,25 +550,17 @@ cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POF cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= -cloud.google.com/go/storagetransfer v1.12.1 h1:W3v9A7MGBN7H9sAFstyciwP/1XEQhUhZfrjclmDnpMs= -cloud.google.com/go/storagetransfer v1.12.1/go.mod h1:hQqbfs8/LTmObJyCC0KrlBw8yBJ2bSFlaGila0qBMk4= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= -cloud.google.com/go/talent v1.8.0 h1:olv+s2g+LGXeJi+MYF1wI44/TwHaVnO0N7PiucVf5ZQ= -cloud.google.com/go/talent v1.8.0/go.mod h1:/gvOzSrtMcfTL/9xWhdYaZATaxUNhQ+L+3ZaGOGs7bA= cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= -cloud.google.com/go/texttospeech v1.11.0 h1:YF/RdNb+jUEp22cIZCvqiFjfA5OxGE+Dxss3mhXU7oQ= -cloud.google.com/go/texttospeech v1.11.0/go.mod h1:7M2ro3I2QfIEvArFk1TJ+pqXJqhszDtxUpnIv/150As= cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= -cloud.google.com/go/tpu v1.8.0 h1:BvMNijOb6Vd46Rr/SR5jWv1MPosOhVsi0UaeAGNjeds= -cloud.google.com/go/tpu v1.8.0/go.mod h1:XyNzyK1xc55WvL5rZEML0Z9/TUHDfnq0uICkQw6rWMo= cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= @@ -786,24 +572,17 @@ cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fp cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/translate v1.12.3 h1:XJ7LipYJi80BCgVk2lx1fwc7DIYM6oV2qx1G4IAGQ5w= -cloud.google.com/go/translate v1.12.3/go.mod h1:qINOVpgmgBnY4YTFHdfVO4nLrSBlpvlIyosqpGEgyEg= cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/video v1.23.3 h1:C2FH+6yr6LCZC4fP0gm9FwJB/SRh5Ul88O5Sc/bL83I= -cloud.google.com/go/video v1.23.3/go.mod h1:Kvh/BheubZxGZDXSb0iO6YX7ZNcaYHbLjnnaC8Qyy3g= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= -cloud.google.com/go/videointelligence v1.12.3 h1:zNTOUQyatGQtnCJ2dR3faRtpWQOlC8wszJqwG5CtwVM= -cloud.google.com/go/videointelligence v1.12.3/go.mod h1:dUA6V+NH7CVgX6TePq0IelVeBMGzvehxKPR4FGf1dtw= -cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= @@ -811,120 +590,50 @@ cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= -cloud.google.com/go/vision/v2 v2.9.3 h1:dPvfDuPqPH+Yscf0f2f1RprvKkoo+N/j0a+IbLYX7Cs= -cloud.google.com/go/vision/v2 v2.9.3/go.mod h1:weAcT8aNYSgrWWVTC2PuJTc7fcXKvUeAyDq8B6HkLSg= cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= -cloud.google.com/go/vmmigration v1.8.3 h1:dpCQq3pj2HnKdbvGTftdWymm3r4ovF7JW5z8xBcO2x4= -cloud.google.com/go/vmmigration v1.8.3/go.mod h1:8CzUpK9eBzohgpL4RvBVtW4sY/sDliVyQonTFQfWcJ4= cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= -cloud.google.com/go/vmwareengine v1.3.3 h1:TfuQr5j7qriINulUMotaC/+27SQaW2thIkF3Gb6VJ38= -cloud.google.com/go/vmwareengine v1.3.3/go.mod h1:G7vz05KGijha0c0dj1INRKyDAaQW8TRMZt/FrfOZVXc= cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= -cloud.google.com/go/vpcaccess v1.8.3 h1:vxVaoFM64M/ht619c4wZNF0iq0QPaMWElOh7Ns4r41A= -cloud.google.com/go/vpcaccess v1.8.3/go.mod h1:bqOhyeSh/nEmLIsIUoCiQCBHeNPNjaK9M3bIvKxFdsY= cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= -cloud.google.com/go/webrisk v1.10.3 h1:yh0v/5n49VO4/i9pYfDm1gLJUj1Ph3Xzegn8WvK9YRA= -cloud.google.com/go/webrisk v1.10.3/go.mod h1:rRAqCA5/EQOX8ZEEF4HMIrLHGTK/Y1hEQgWMnih+jAw= cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= -cloud.google.com/go/websecurityscanner v1.7.3 h1:/uxhVCWKXzPw5pVfnBOVjaSiQ6Bm0tDExDOCLV40thw= -cloud.google.com/go/websecurityscanner v1.7.3/go.mod h1:gy0Kmct4GNLoCePWs9xkQym1D7D59ld5AjhXrjipxSs= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= -cloud.google.com/go/workflows v1.13.3 h1:lNFDMranJymDEB7cTI7DI9czbc1WU0RWY9KCEv9zuDY= -cloud.google.com/go/workflows v1.13.3/go.mod h1:Xi7wggEt/ljoEcyk+CB/Oa1AHBCk0T1f5UH/exBB5CE= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= -gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= -github.com/99designs/gqlgen v0.17.36 h1:u/o/rv2SZ9s5280dyUOOrkpIIkr/7kITMXYD3rkJ9go= -github.com/99designs/gqlgen v0.17.36/go.mod h1:6RdyY8puhCoWAQVr2qzF2OMVfudQzc8ACxzpzluoQm4= -github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= -github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= -github.com/Abirdcfly/dupword v0.0.11 h1:z6v8rMETchZXUIuHxYNmlUAuKuB21PeaSymTed16wgU= -github.com/Abirdcfly/dupword v0.0.11/go.mod h1:wH8mVGuf3CP5fsBTkfWwwwKTjDnVVCxtU8d8rgeVYXA= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Antonboom/errname v0.1.9 h1:BZDX4r3l4TBZxZ2o2LNrlGxSHran4d1u4veZdoORTT4= -github.com/Antonboom/errname v0.1.9/go.mod h1:nLTcJzevREuAsgTbG85UsuiWpMpAqbKD1HNZ29OzE58= -github.com/Antonboom/nilnil v0.1.4 h1:yWIfwbCRDpJiJvs7Quz55dzeXCgORQyAG29N9/J5H2Q= -github.com/Antonboom/nilnil v0.1.4/go.mod h1:iOov/7gRcXkeEU+EMGpBu2ORih3iyVEiWjeste1SJm8= -github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 h1:j8BorDEigD8UFOSZQiSqAMOOleyQOOQPnUAwV+Ls1gA= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2/go.mod h1:dmXQgZuiSubAecswZE+Sm8jkvEa7kQgTPVRvwL/nd0E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= -github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= -github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= -github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= -github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= -github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/appsec-internal-go v1.9.0 h1:cGOneFsg0JTRzWl5U2+og5dbtyW3N8XaYwc5nXe39Vw= github.com/DataDog/appsec-internal-go v1.9.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g= github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1 h1:XHITEDEb6NVc9n+myS8KJhdK0vKOvY0BTWSFrFynm4s= github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1/go.mod h1:lzCtnMSGZm/3RMk5RBRW/6IuK1TNbDXx1ttHTxN5Ykc= -github.com/DataDog/datadog-agent/comp/trace/compression/def v0.64.0-rc.1 h1:yjO0qOUDDjC6i+w7eKGpXkkgPkpGP6D3ln1clFav62s= -github.com/DataDog/datadog-agent/comp/trace/compression/def v0.64.0-rc.1/go.mod h1:+BnlaBR5kMqTFYwDEc1QW0f6zvLJKnwBAUbeC68HaVs= -github.com/DataDog/datadog-agent/comp/trace/compression/impl-gzip v0.64.0-rc.1 h1:Rx2o4Sk1IwkpZbZu5PoLaai8rQjegwJ/eieEjryaU10= -github.com/DataDog/datadog-agent/comp/trace/compression/impl-gzip v0.64.0-rc.1/go.mod h1:mW8ei9lGT6E3uDsBLChNYIZ/GOUZNEwZh3Z+RaS/df4= -github.com/DataDog/datadog-agent/comp/trace/compression/impl-zstd v0.64.0-rc.1 h1:ADqb0FHp5TsTwk31aWy8hOp0XmizOsEKG6ByEIMfXYY= -github.com/DataDog/datadog-agent/comp/trace/compression/impl-zstd v0.64.0-rc.1/go.mod h1:qtNBiygMkl/zGi5v+ejj65hRoCQLRJadybMCFidfDKQ= github.com/DataDog/datadog-agent/pkg/obfuscate v0.64.0-rc.1 h1:63L66uiNazsZs1DCmb5aDv/YAkCqn6xKqc0aYeATkQ8= github.com/DataDog/datadog-agent/pkg/obfuscate v0.64.0-rc.1/go.mod h1:3BS4G7V1y7jhSgrbqPx2lGxBb/YomYwUP0wjwr+cBHc= github.com/DataDog/datadog-agent/pkg/proto v0.64.0-rc.1 h1:8+4sv0i+na4QMjggZrQNFspbVHu7iaZU6VWeupPMdbA= @@ -933,12 +642,8 @@ github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1 h1:MpUmwDTz github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1/go.mod h1:QHiOw0sFriX2whwein+Puv69CqJcbOQnocUBo2IahNk= github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1 h1:5PbiZw511B+qESc7PxxWY5ubiBtVnLFqC+UZKZAB3xo= github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1/go.mod h1:AkapH6q9UZLoRQuhlOPiibRFqZtaKPMwtzZwYjjzgK0= -github.com/DataDog/datadog-agent/pkg/util/cgroups v0.64.0-rc.1 h1:GqHjzbYflWAKQLv/yoNeC54lsrcZgxbAlX2nzfJzx8M= -github.com/DataDog/datadog-agent/pkg/util/cgroups v0.64.0-rc.1/go.mod h1:xQ8Es4XKlrWKUYp/ZY67vXRAjOLmfrGqMlNZ471y/vM= github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1 h1:5UHDao4MdRwRsf4ZEvMSbgoujHY/2Aj+TQ768ZrPXq8= github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1/go.mod h1:ZEm+kWbgm3alAsoVbYFM10a+PIxEW5KoVhV3kwiCuxE= -github.com/DataDog/datadog-agent/pkg/util/pointer v0.64.0-rc.1 h1:Qiiq60ysVOcEO3cO/2ffLyy1ZEZHU2ZybuJBACVvBSI= -github.com/DataDog/datadog-agent/pkg/util/pointer v0.64.0-rc.1/go.mod h1:cY4100zn21kBb748mG0hDv9BfswD+tUw3p2M4XdUzAE= github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1 h1:yqzXiCXrBXsQrbsFCTele7SgM6nK0bElDmBM0lsueIE= github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1/go.mod h1:9ZfE6J8Ty8xkgRuoH1ip9kvtlq6UaHwPOqxe9NJbVUE= github.com/DataDog/datadog-agent/pkg/version v0.64.0-rc.1 h1:eg+XW2CzOwFa//bjoXiw4xhNWWSdEJbMSC4TFcx6lVk= @@ -959,14 +664,6 @@ github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0 h1:GlvoS github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0/go.mod h1:mYQmU7mbHH6DrCaS8N6GZcxwPoeNfyuopUoLQltwSzs= github.com/DataDog/sketches-go v1.4.7 h1:eHs5/0i2Sdf20Zkj0udVFWuCrXGRFig2Dcfm5rtcTxc= github.com/DataDog/sketches-go v1.4.7/go.mod h1:eAmQ/EBmtSO+nQp7IZMZVRPT4BQTmIc5RZQ+deGlTPM= -github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= -github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Djarvur/go-err113 v0.1.0 h1:uCRZZOdMQ0TZPHYTdYpoC0bLYJKPEHPUJ8MeAa51lNU= -github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 h1:+r1rSv4gvYn0wmRjC8X7IAzX8QezqtFV9m0MUHFJgts= -github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0/go.mod h1:b3g59n2Y+T5xmcxJL+UEG2f8cQploZm1mR/v6BW0mU0= -github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible h1:juIaKLLVhqzP55d8x4cSVgwyQv76Z55/fRv/UBr2KkQ= -github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible/go.mod h1:BB1eHdMLYEFuFdBlRMb0N7YGVdM5s6Pt0njxgvfbGGs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= @@ -975,74 +672,33 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 h1:ig/FpDD2JofP/NExKQUbn7uOSZzJAQqogfqluZK4ed4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= -github.com/IBM/sarama v1.40.0 h1:QTVmX+gMKye52mT5x+Ve/Bod2D0Gy7ylE2Wslv+RHtc= -github.com/IBM/sarama v1.40.0/go.mod h1:6pBloAs1WanL/vsq5qFTyTGulJUntZHhMLOUYEIs9mg= -github.com/Intevation/gval v1.3.0 h1:+Ze5sft5MmGbZrHj06NVUbcxCb67l9RaPTLMNr37mjw= -github.com/Intevation/gval v1.3.0/go.mod h1:xmGyGpP5be12EL0P12h+dqiYG8qn2j3PJxIgkoOHO5o= -github.com/Intevation/jsonpath v0.2.1 h1:rINNQJ0Pts5XTFEG+zamtdL7l9uuE1z0FBA+r55Sw+A= -github.com/Intevation/jsonpath v0.2.1/go.mod h1:WnZ8weMmwAx/fAO3SutjYFU+v7DFreNYnibV7CiaYIw= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= -github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= -github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= -github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/OpenPeeDeeP/depguard v1.1.1 h1:TSUznLjvp/4IUP+OQ0t/4jF4QUyxIcVX8YnghZdunyA= -github.com/OpenPeeDeeP/depguard v1.1.1/go.mod h1:JtAMzWkmFEzDPyAd+W0NHl1lvpQKTvT9jnRVsohBKpc= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= -github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= -github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= -github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= -github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= -github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= @@ -1050,30 +706,12 @@ github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= -github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= -github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= -github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= -github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= -github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= -github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= -github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= -github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= -github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/ammario/tlru v0.4.0 h1:sJ80I0swN3KOX2YxC6w8FbCqpQucWdbb+J36C05FPuU= github.com/ammario/tlru v0.4.0/go.mod h1:aYzRFu0XLo4KavE9W8Lx7tzjkX+pAApz+NgcKYIFUBQ= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= @@ -1081,71 +719,30 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/anthropics/anthropic-sdk-go v1.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0= github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= -github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= -github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= -github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= -github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= -github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30xLN2sUZcMXl50hg+PJCIDdJgIvIbVcKqLJ/ZrtM= -github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8= -github.com/aquasecurity/defsec v0.94.1 h1:lk44bfUltm0f0Dw4DbO3Ka9d/bf3N8cWclSdHXMyKF4= -github.com/aquasecurity/defsec v0.94.1/go.mod h1:wiX9BX0SOG0ZWjVIPYGPl46fyO3Gu8lJnk4rmhFR7IA= -github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM= -github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s= -github.com/aquasecurity/go-npm-version v0.0.1 h1:2i/MM+A4KI8AJrqJa/Cwsa4qyljA8S/qngPyQiIVHcA= -github.com/aquasecurity/go-npm-version v0.0.1/go.mod h1:hxbJZtKlO4P8sZ9nztizR6XLoE33O+BkPmuYQ4ACyz0= -github.com/aquasecurity/go-pep440-version v0.0.1 h1:8VKKQtH2aV61+0hovZS3T//rUF+6GDn18paFTVS0h0M= -github.com/aquasecurity/go-pep440-version v0.0.1/go.mod h1:3naPe+Bp6wi3n4l5iBFCZgS0JG8vY6FT0H4NGhFJ+i4= github.com/aquasecurity/go-version v0.0.1 h1:4cNl516agK0TCn5F7mmYN+xVs1E3S45LkgZk3cbaW2E= github.com/aquasecurity/go-version v0.0.1/go.mod h1:s1UU6/v2hctXcOa3OLwfj5d9yoXHa3ahf+ipSwEvGT0= github.com/aquasecurity/iamgo v0.0.10 h1:t/HG/MI1eSephztDc+Rzh/YfgEa+NqgYRSfr6pHdSCQ= github.com/aquasecurity/iamgo v0.0.10/go.mod h1:GI9IQJL2a+C+V2+i3vcwnNKuIJXZ+HAfqxZytwy+cPk= github.com/aquasecurity/jfather v0.0.8 h1:tUjPoLGdlkJU0qE7dSzd1MHk2nQFNPR0ZfF+6shaExE= github.com/aquasecurity/jfather v0.0.8/go.mod h1:Ag+L/KuR/f8vn8okUi8Wc1d7u8yOpi2QTaGX10h71oY= -github.com/aquasecurity/table v1.10.0 h1:gPWV28qp9XSlvXdT3ku8yKQoZE6II0vsmegKpW+dB08= -github.com/aquasecurity/table v1.10.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw= -github.com/aquasecurity/testdocker v0.0.0-20240730042311-4642e94c7fc8 h1:b43UVqYjz7qDqK+cVOtF2Lk6CxjytYItP6Pgf3wGsNE= -github.com/aquasecurity/testdocker v0.0.0-20240730042311-4642e94c7fc8/go.mod h1:wXA9k3uuaxY3yu7gxrxZDPo/04FEMJtwyecdAlYrEIo= -github.com/aquasecurity/tml v0.6.1 h1:y2ZlGSfrhnn7t4ZJ/0rotuH+v5Jgv6BDDO5jB6A9gwo= -github.com/aquasecurity/tml v0.6.1/go.mod h1:OnYMWY5lvI9ejU7yH9LCberWaaTBW7hBFsITiIMY2yY= -github.com/aquasecurity/trivy-checks v1.10.0 h1:Q0FWsYy/uwvr/icRSOzNu55yDZ1ME8hZlpglNs62ZfE= -github.com/aquasecurity/trivy-checks v1.10.0/go.mod h1:/b633SOFNp8RjkxSq+FOg4SgxjklUp+BIQEyTWCnN1k= -github.com/aquasecurity/trivy-db v0.0.0-20250227071930-8bd8a9b89e2d h1:T16WrTi21YsMLQVhtp1r1hOIYK3x4BjnftpL9cp64Eo= -github.com/aquasecurity/trivy-db v0.0.0-20250227071930-8bd8a9b89e2d/go.mod h1:4bTsQPtMBN8v+UfUlE1aQBN1imftefnDafHBF85+aT8= github.com/aquasecurity/trivy-iac v0.8.0 h1:NKFhk/BTwQ0jIh4t74V8+6UIGUvPlaxO9HPlSMQi3fo= github.com/aquasecurity/trivy-iac v0.8.0/go.mod h1:ARiMeNqcaVWOXJmp8hmtMnNm/Jd836IOmDBUW5r4KEk= -github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48 h1:JVgBIuIYbwG+ekC5lUHUpGJboPYiCcxiz06RCtz8neI= -github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48/go.mod h1:Ldya37FLi0e/5Cjq2T5Bty7cFkzUDwTcPeQua+2M8i8= -github.com/aquasecurity/trivy-kubernetes v0.8.2 h1:8QP8fbUeC5y0kETDpk04XK52hYF6PsJOkd6HrbVnJqo= -github.com/aquasecurity/trivy-kubernetes v0.8.2/go.mod h1:O2SMbA3pnShkOCxBFT04ANYyDEvACbmd5t/XMH4uAFM= -github.com/aquasecurity/trivy-policies v0.8.0 h1:LvmIdw/DfTF72Lc8L+CKLYzfb5BFYzLBGFFR95PKC74= -github.com/aquasecurity/trivy-policies v0.8.0/go.mod h1:qF/t59pgK/0JTV6tXaeA3Iw3opzoMgzGCDcTDBmqb30= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= -github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/ashanbrown/forbidigo v1.5.1 h1:WXhzLjOlnuDYPYQo/eFlcFMi8X/kLfvWLYu6CSoebis= -github.com/ashanbrown/forbidigo v1.5.1/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= -github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= -github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= github.com/aslilac/afero v0.0.0-20250403163713-f06e86036696 h1:7hAl/81gNUjmSCqJYKe1aTIVY4myjapaSALdCko19tI= github.com/aslilac/afero v0.0.0-20250403163713-f06e86036696/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -1157,8 +754,6 @@ github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= @@ -1167,48 +762,16 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mln github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.1 h1:yg6nrV33ljY6CppoRnnsKLqIZ5ExNdQOGRBGNfc56Yw= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.1/go.mod h1:hGdIV5nndhIclFFvI1apVfQWn9ZKqedykZ1CtLZd03E= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.10 h1:fdLh7eMf5mxtggx2nG0+cFkaiRK+ULCOPK3qq8eTje4= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.10/go.mod h1:uBca+/1aH5v/RYWXqyymLrsbmx1vU9bBxeurlC627Gc= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.4 h1:x3V1JRHq7q9RUbDpaeNpLH7QoipGpCo3fdnMMuSeABU= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.4/go.mod h1:aryF4jxgjhbqpdhj8QybUZI3xYrX8MQIKm4WbOv8Whg= -github.com/aws/aws-sdk-go-v2/service/ebs v1.22.1 h1:SeDJWG4pmye+/aO6k+zt9clPTUy1MXqUmkW8rbAddQg= -github.com/aws/aws-sdk-go-v2/service/ebs v1.22.1/go.mod h1:wRzaW0v9GGQS0h//wpsVDw3Hah5gs5UP+NxoyGeZIGM= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.218.0 h1:QPYsTfcPpPhkF+37pxLcl3xbQz2SRxsShQNB6VCkvLo= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.218.0/go.mod h1:ouvGEfHbLaIlWwpDpOVWPWR+YwO0HDv3vm5tYLq8ImY= -github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 h1:E+UTVTDH6XTSjqxHWRuY8nB6s+05UllneWxnycplHFk= -github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU= -github.com/aws/aws-sdk-go-v2/service/eventbridge v1.20.4 h1:G18wotYZxZ0A5tkqKv6FHCjsF86UQrqNHy5LS+T7JWM= -github.com/aws/aws-sdk-go-v2/service/eventbridge v1.20.4/go.mod h1:XlbY5AGZhlipCdhRorT18/HEThKAxo51hMmhixreJoM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.34 h1:JlxVMFDHivlhNOIxd2O/9z4O0wC2zIC4lRB71lejVHU= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.34/go.mod h1:CDPcT6pljRaqz1yLsOgPUvOPOczFvXuJxOKzDzAbF0c= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= -github.com/aws/aws-sdk-go-v2/service/kinesis v1.18.4 h1:UohaQds+Puk9BEbvncXkZduIGYImxohbFpVmSoymXck= -github.com/aws/aws-sdk-go-v2/service/kinesis v1.18.4/go.mod h1:HnjgmL8TNmYtGcrA3N6EeCnDvlX6CteCdUbZ1wV8QWQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 h1:BRXS0U76Z8wfF+bnkilA2QwpIch6URlm++yPUt9QPmQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3/go.mod h1:bNXKFFyaiVvWuR6O16h/I1724+aXe/tAkA9/QS01t5k= -github.com/aws/aws-sdk-go-v2/service/sfn v1.19.4 h1:yIyFY2kbCOoHvuivf9minqnP2RLYJgmvQRYxakIb2oI= -github.com/aws/aws-sdk-go-v2/service/sfn v1.19.4/go.mod h1:uWCH4ATwNrkRO40j8Dmy7u/Y1/BVWgCM+YjBNYZeOro= -github.com/aws/aws-sdk-go-v2/service/sns v1.21.4 h1:Asj098jPfIZYzAbk4xVFwVBGij5hgMcli0d+5Pe4aZA= -github.com/aws/aws-sdk-go-v2/service/sns v1.21.4/go.mod h1:bbB779DXXOnPXvB7F3dP7AjuV1Eyr7fNyrA058ExuzY= -github.com/aws/aws-sdk-go-v2/service/sqs v1.24.4 h1:bp8KUUx15mnLMe8SSJqO/kYEn0C2kKfWq/M9SRK9i1E= -github.com/aws/aws-sdk-go-v2/service/sqs v1.24.4/go.mod h1:c1AF/ac4k4xz32FprEk6AqqGFH/Fkub9VUPSrASlllA= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 h1:hgSBvRT7JEWx2+vEGI9/Ld5rZtl7M5lu8PqdvOmbRHw= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4/go.mod h1:v7NIzEFIHBiicOMaMTuEmbnzGnqW0d+6ulNALul6fYE= github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= @@ -1225,10 +788,6 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/bazelbuild/rules_go v0.44.2 h1:H2nzlC9VLKeVW1D90bahFSszpDE5qvtKr95Nz7BN0WQ= -github.com/bazelbuild/rules_go v0.44.2/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= -github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw= -github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= 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/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= @@ -1247,99 +806,47 @@ github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5k github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg= github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY= github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI= -github.com/bep/helpers v0.5.0 h1:rneezhnG7GzLFlsEWO/EnleaBRuluBDGFimalO6Y50o= -github.com/bep/helpers v0.5.0/go.mod h1:dSqCzIvHbzsk5YOesp1M7sKAq5xUcvANsRoKdawxH4Q= github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k= github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8= github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8= github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk= github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= -github.com/bep/mclib v1.20400.20402 h1:olpCE2WSPpOAbFE1R4hnftSEmQ34+xzy2HRzd0m69rA= -github.com/bep/mclib v1.20400.20402/go.mod h1:pkrk9Kyfqg34Uj6XlDq9tdEFJBiL1FvCoCgVKRzw1EY= github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE= github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro= -github.com/bep/simplecobra v0.6.0 h1:PpY/0PvYp6jt4OC/9SGoNPi6HzvpYzu8IPogVV6Xk90= -github.com/bep/simplecobra v0.6.0/go.mod h1:q0ecBAefJZYpzgkbPbQ901hzA98g3ZvCZWZRhzNtB5o= github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c h1:C4UZIaS+HAw+X6jGUsoP2ZbM99PuqhCttjomg1yhNAI= -github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo= -github.com/bkielbasa/cyclop v1.2.0 h1:7Jmnh0yL2DjKfw28p86YTd/B4lRGcNuu12sKE35sM7A= -github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= -github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= -github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= -github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/bobuhiro11/gokvm v0.0.8-0.20231003020000-f53faca69d28 h1:pO0VjeSk0Tcd0NIHxgD6Gyd8T0pw79hs6Usr2Cwr16M= -github.com/bobuhiro11/gokvm v0.0.8-0.20231003020000-f53faca69d28/go.mod h1:xQjzvEq5CXolwHJyswTQXuGXNjF3bYavvXZXDZS+FTI= -github.com/bombsimon/wsl/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU= -github.com/bombsimon/wsl/v3 v3.4.0/go.mod h1:KkIB+TXkqy6MvK9BDZVbZxKNYsE1/oLRJbIFtf14qqo= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285 h1:Dr+ezPI5ivhMn/3WOoB86XzMhie146DNaBbhaQWZHMY= -github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM= github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ= -github.com/breml/bidichk v0.2.4 h1:i3yedFWWQ7YzjdZJHnPo9d/xURinSq3OM+gyM43K4/8= -github.com/breml/bidichk v0.2.4/go.mod h1:7Zk0kRFt1LIZxtQdl9W9JwGAcLTTkOs+tN7wuEYGJ3s= -github.com/breml/errchkjson v0.3.1 h1:hlIeXuspTyt8Y/UmP5qy1JocGNR00KQHgfaNtRAjoxQ= -github.com/breml/errchkjson v0.3.1/go.mod h1:XroxrzKjdiutFyW3nWhw34VGg7kiMsDQox73yWCGI2U= -github.com/butuzov/ireturn v0.2.0 h1:kCHi+YzC150GE98WFuZQu9yrTn6GEydO2AuPLbTgnO4= -github.com/butuzov/ireturn v0.2.0/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= -github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= -github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls= -github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= -github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= -github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g= github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets= -github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= -github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= -github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= -github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= -github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= -github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk= @@ -1354,23 +861,14 @@ github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Y github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 h1:W9o46d2kbNL06lq7UNDPV0zYLzkrde/bjIqO02eoll0= -github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8/go.mod h1:gakxgyXaaPkxvLw1XQxNGK4I37ys9iBRzNUx/B7pUCo= -github.com/checkpoint-restore/go-criu/v6 v6.3.0 h1:mIdrSO2cPNWQY1truPg6uHLXyKHk3Z5Odx4wjKOASzA= -github.com/checkpoint-restore/go-criu/v6 v6.3.0/go.mod h1:rrRTN/uSwY2X+BPRl/gkulo9gsKOSAeVp9/K2tv7xZI= -github.com/cheggaaa/pb v1.0.27 h1:wIkZHkNfC7R6GI5w7l/PdAdzXzlrbcI3p8OAlnkTsnc= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= -github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI= -github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ= github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs= github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0= github.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= @@ -1380,21 +878,13 @@ github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyM github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= -github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -1406,10 +896,6 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/cockroachdb/cockroach-go/v2 v2.1.1 h1:3XzfSMuUT0wBe1a3o5C0eOTcArhmmFAg2Jzh/7hhKqo= -github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= -github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= -github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo= github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwuWwPHPYoCZ/KLAjHv5g4h2MS4f2/MTI= @@ -1418,7 +904,6 @@ github.com/coder/clistat v1.0.0 h1:MjiS7qQ1IobuSSgDnxcCSyBPESs44hExnh2TEqMcGnA= github.com/coder/clistat v1.0.0/go.mod h1:F+gLef+F9chVrleq808RBxdaoq52R4VLopuLdAsh8Y4= github.com/coder/flog v1.1.0 h1:kbAes1ai8fIS5OeV+QAnKBQE22ty1jRF/mcAwHpLBa4= github.com/coder/flog v1.1.0/go.mod h1:UQlQvrkJBvnRGo69Le8E24Tcl5SJleAAR7gYEHzAmdQ= -github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1 h1:UqBrPWSYvRI2s5RtOul20JukUEpu4ip9u7biBL+ntgk= github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322 h1:m0lPZjlQ7vdVpRBPKfYIFlmgevoTkBxB10wv6l2gOaU= github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322/go.mod h1:rOLFDDVKVFiDqZFXoteXc97YXx7kFi9kYqR+2ETPkLQ= @@ -1454,73 +939,25 @@ github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Au github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0/go.mod h1:qANbdpqyAGlo2bg+4gQKPj24H1ZWa3bQU2Q5/bV5B3Y= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818 h1:bNhUTaKl3q0bFn78bBRq7iIwo72kNTvUD9Ll5TTzDDk= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818/go.mod h1:fAlLM6hUgnf4Sagxn2Uy5Us0PBgOYWz+63HwHUVGEbw= -github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q= -github.com/confluentinc/confluent-kafka-go v1.9.2/go.mod h1:ptXNqsuDfYbAE/LBW6pnwWZElUoWxHoV8E43DCrliyo= -github.com/confluentinc/confluent-kafka-go/v2 v2.4.0 h1:NbOku86JJlsRJPJKE0snNsz6D1Qr4j5VR/lticrLZrY= -github.com/confluentinc/confluent-kafka-go/v2 v2.4.0/go.mod h1:E1dEQy50ZLfqs7T9luxz0rLxaeFZJZE92XvApJOr/Rk= -github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ= -github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= -github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= -github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= -github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= -github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= -github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= -github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= -github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.1.1 h1:znnkm7Ajz8lg8BcIPMhc/9yjBRN3B+OkNKqKisKfwwM= -github.com/containerd/containerd/v2 v2.1.1/go.mod h1:zIfkQj4RIodclYQkX7GSSswSwgP8d/XxDOtOAoSDIGU= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= -github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= -github.com/containerd/go-runc v1.0.0 h1:oU+lLv1ULm5taqgV/CJivypVODI4SUz1znWjv3nNYS0= -github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= -github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= -github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= -github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= -github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= -github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= -github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= -github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= -github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= -github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= -github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= -github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= -github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 h1:2Dx4IHfC1yHWI12AxQDJM1QbRCDfk6M+blLzlZCXdrc= -github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= -github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= -github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= -github.com/daixiang0/gci v0.10.1 h1:eheNA3ljF6SxnPD/vE4lCBusVHmV3Rs3dkKvFrJ7MR0= -github.com/daixiang0/gci v0.10.1/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= -github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= -github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc= github.com/dave/dst v0.27.2/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk= @@ -1531,10 +968,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e h1:L+XrFvD0vBIBm+Wf9sFN6aU395t7JROoai0qXZraA4U= github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU= -github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= -github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= -github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= -github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= @@ -1542,18 +975,10 @@ github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= -github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= -github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= -github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= -github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= -github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ= -github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw= github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -1562,57 +987,22 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= -github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd h1:QMSNEh9uQkDjyPwu/J541GgSH+4hw+0skJDIj9HJ3mE= github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= -github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ= -github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= -github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= -github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= -github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= -github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= -github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= -github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI= -github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -github.com/eapache/go-resiliency v1.4.0 h1:3OK9bWpPk5q6pbFAaYSEwD9CLUSHG8bnZuqX2yMt3B0= -github.com/eapache/go-resiliency v1.4.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= -github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= -github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= -github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 h1:8EXxF+tCLqaVk8AOC29zl2mnhQjwyLxxOTuhUazWRsg= github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4/go.mod h1:I5sHm0Y0T1u5YjlyqC5GVArM7aNZRUYtTjmJ8mPJFds= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8= -github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elastic/elastic-transport-go/v8 v8.1.0 h1:NeqEz1ty4RQz+TVbUrpSU7pZ48XkzGWQj02k5koahIE= -github.com/elastic/elastic-transport-go/v8 v8.1.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI= -github.com/elastic/go-elasticsearch/v6 v6.8.5 h1:U2HtkBseC1FNBmDr0TR2tKltL6FxoY+niDAlj5M8TK8= -github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= -github.com/elastic/go-elasticsearch/v7 v7.17.1 h1:49mHcHx7lpCL8cW1aioEwSEVKQF3s+Igi4Ye/QTWwmk= -github.com/elastic/go-elasticsearch/v7 v7.17.1/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4= -github.com/elastic/go-elasticsearch/v8 v8.4.0 h1:Rn1mcqaIMcNT43hnx2H62cIFZ+B6mjWtzj85BDKrvCE= -github.com/elastic/go-elasticsearch/v8 v8.4.0/go.mod h1:yY52i2Vj0unLz+N3Nwx1gM5LXwoj3h2dgptNGBYkMLA= github.com/elastic/go-sysinfo v1.15.1 h1:zBmTnFEXxIQ3iwcQuk7MzaUotmKRp3OabbbWM8TdzIQ= github.com/elastic/go-sysinfo v1.15.1/go.mod h1:jPSuTgXG+dhhh0GKIyI2Cso+w5lPJ5PvVqKlL8LV/Hk= github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY= @@ -1621,10 +1011,6 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1X github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.21.2 h1:OLDgvZKuofk4em9fT5tFG5j4jE1/hXnX75UMvcrL4AA= github.com/emersion/go-smtp v0.21.2/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= -github.com/emicklei/go-restful v2.16.0+incompatible h1:rgqiKNjTnFQA6kkhFe16D8epTksy9HQ1MyrbDXSdYhM= -github.com/emicklei/go-restful v2.16.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -1652,20 +1038,8 @@ github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfU github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/esimonov/ifshort v1.0.4 h1:6SID4yGWfRae/M7hkVDVVyppy8q/v9OuxNdmjLQStBA= -github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0= -github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= -github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= -github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/evanw/esbuild v0.25.3 h1:4JKyUsm/nHDhpxis4IyWXAi8GiyTwG1WdEp6OhGVE8U= github.com/evanw/esbuild v0.25.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= -github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= -github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72 h1:0eU/faU2oDIB2BkQVM02hgRLJjGzzUuRf19HUhp0394= -github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -1674,22 +1048,13 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= -github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fergusstrange/embedded-postgres v1.31.0 h1:JmRxw2BcPRcU141nOEuGXbIU6jsh437cBB40rmftZSk= github.com/fergusstrange/embedded-postgres v1.31.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk= -github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= -github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= -github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e h1:Ss/B3/5wWRh8+emnK0++g5zQzwDTi30W10pKxKc4JXI= -github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e/go.mod h1:HyVoz1Mz5Co8TFO8EupIdlcpwShBmY98dkT2xeHkvEI= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= -github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -1699,34 +1064,20 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fsouza/fake-gcs-server v1.17.0 h1:OeH75kBZcZa3ZE+zz/mFdJ2btt9FgqfjI7gIh9+5fvk= -github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= -github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg= -github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw= github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a h1:fwNLHrP5Rbg/mGSXCjtPdpbqv2GucVTA/KMi8wEm6mE= github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a/go.mod h1:/WeFVhhxMOGypVKS0w8DUJxUBbHypnWkUVnW7p5c9Pw= github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE= github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= -github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= @@ -1737,18 +1088,10 @@ github.com/go-chi/hostrouter v0.2.0 h1:GwC7TZz8+SlJN/tV/aeJgx4F+mI5+sp+5H1PelQUj github.com/go-chi/hostrouter v0.2.0/go.mod h1:pJ49vWVmtsKRKZivQx0YMYv4h0aX+Gcn6V23Np9Wf1s= github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= -github.com/go-critic/go-critic v0.8.0 h1:4zOcpvDoKvBOl+R1W81IBznr78f8YaE4zKXkfDVxGGA= -github.com/go-critic/go-critic v0.8.0/go.mod h1:5TjdkPI9cu/yKbYS96BTsslihjKd6zg6vd8O9RZXj2s= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= @@ -1756,23 +1099,16 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= -github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= @@ -1784,38 +1120,19 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4 github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= -github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= -github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pg/pg/v10 v10.11.1 h1:vYwbFpqoMpTDphnzIPshPPepdy3VpzD8qo29OFKp4vo= -github.com/go-pg/pg/v10 v10.11.1/go.mod h1:ExJWndhDNNftBdw1Ow83xqpSf4WMSJK8urmXD5VXS1I= -github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= -github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -1824,44 +1141,18 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= -github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= -github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= -github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= -github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= -github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= -github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= -github.com/go-toolsmith/astequal v1.1.0 h1:kHKm1AWqClYn15R0K1KKE4RG614D46n+nqUQ06E1dTw= -github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= -github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= -github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= -github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= -github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= -github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= -github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= -github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= -github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= -github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= -github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= -github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -1871,20 +1162,8 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.15.23 h1:WS0GAX1uNPDLUvLkNU2vXq6oTnsmfVFocjQ/4qA48qo= -github.com/goccy/go-yaml v1.15.23/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/gocql/gocql v1.6.0 h1:IdFdOTbnpbd0pDhl4REKQDM+Q0SzKXQ1Yh+YZZ8T/qU= -github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= -github.com/gocsaf/csaf/v3 v3.2.0 h1:LF9j1ou4Cm5MT4+oHbk16Ae0hSmCztwPJqciTapVNIU= -github.com/gocsaf/csaf/v3 v3.2.0/go.mod h1:EpUCrQg69i+Y66MphmQvVbcj333GFLjXOYHg1zoXVso= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= -github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= @@ -1907,10 +1186,6 @@ github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XG github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0= -github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95 h1:sgew0XCnZwnzpWxTt3V8LLiCO7OQi3C6dycaE67wfkU= -github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95/go.mod h1:bOlVlCa1/RajcHpXkrUXPSHB/Re1UnlXxD1Qp8SKOd8= -github.com/gojuno/minimock/v3 v3.0.8 h1:+L+WvGoTvPB4YCbkMI5WFyp3Mvz6Z5ubBuTXWMhmwmA= -github.com/gojuno/minimock/v3 v3.0.8/go.mod h1:TPKxc8tiB8O83YH2//pOzxvEjaI3TMhd6ev/GmlMiYA= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= @@ -1918,11 +1193,6 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= -github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1962,45 +1232,16 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6J5HIP8ZtyMdiDscjMLfRBSPuzVVeo= -github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= -github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 h1:amWTbTGqOZ71ruzrdA+Nx5WA3tV1N0goTspwmKCQvBY= -github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2/go.mod h1:9wOXstvyDRshQ9LggQuzBCGysxs3b6Uo/1MvYCR2NMs= -github.com/golangci/golangci-lint v1.52.2 h1:FrPElUUI5rrHXg1mQ7KxI1MXPAw5lBVskiz7U7a8a1A= -github.com/golangci/golangci-lint v1.52.2/go.mod h1:S5fhC5sHM5kE22/HcATKd1XLWQxX+y7mHj8B5H91Q/0= -github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= -github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= -github.com/golangci/misspell v0.4.0 h1:KtVB/hTK4bbL/S6bs64rYyk8adjmh1BygbBiaAiX+a0= -github.com/golangci/misspell v0.4.0/go.mod h1:W6O/bwV6lGDxUCChm2ykw9NQdd5bYd1Xkjo88UcWyJc= -github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 h1:DIPQnGy2Gv2FSA4B/hh8Q7xx3B7AIDk3DAMeHclH1vQ= -github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6/go.mod h1:0AKcRCkMoKvUvlf89F6O7H2LYdhr1zBh736mBItOdRs= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 h1:4txT5G2kqVAKMjzidIabL/8KqjIK71yj30YOeuxLn10= github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= -github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= -github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/certificate-transparency-go v1.1.8 h1:LGYKkgZF7satzgTak9R4yzfJXEeYVAjV6/EAEJOf1to= -github.com/google/certificate-transparency-go v1.1.8/go.mod h1:bV/o8r0TBKRf1X//iiiSgWrvII4d7/8OiA+3vG26gI8= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= -github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -2019,34 +1260,16 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= -github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= -github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= -github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= -github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= -github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 h1:DdHws/YnnPrSywrjNYu2lEHqYHWp/LnEx56w59esd54= github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405/go.mod h1:4RgUDSnsxP19d65zJWqvqJ/poJxBCvmna50eXmIvoR8= -github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= -github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= -github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= -github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= -github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= -github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/go-tpm v0.9.1-0.20230914180155-ee6cbcd136f8 h1:g9RVRZdQrNEK2E94RcFescvXFC9afWsFar4IIdejP34= -github.com/google/go-tpm v0.9.1-0.20230914180155-ee6cbcd136f8/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4= -github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= -github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA= -github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -2073,24 +1296,15 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/rpmpack v0.0.0-20221120200012-98b63d62fd77 h1:+C0+foB1Bm0WYdbaDIuUGEVG1Eqx9WWcGUoJBSLdZo0= -github.com/google/rpmpack v0.0.0-20221120200012-98b63d62fd77/go.mod h1:Ys71Xf3d5OC4Ww06S5YJcbk+Eh/1CsB+rZibR7NDEA4= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= -github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= @@ -2111,62 +1325,20 @@ github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57Q github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 h1:9alfqbrhuD+9fLZ4iaAVwhlp5PEhmnBt7yvK2Oy5C1U= -github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= -github.com/goreleaser/chglog v0.4.2 h1:afmbT1d7lX/q+GF8wv3a1Dofs2j/Y9YkiCpGemWR6mI= -github.com/goreleaser/chglog v0.4.2/go.mod h1:u/F03un4hMCQrp65qSWCkkC6T+G7YLKZ+AM2mITE47s= -github.com/goreleaser/fileglob v0.3.1 h1:OTFDWqUUHjQazk2N5GdUqEbqT/grBnRARaAXsV07q1Y= -github.com/goreleaser/fileglob v0.3.1/go.mod h1:kNcPrPzjCp+Ox3jmXLU5QEsjhqrtLBm6OnXAif8KRl8= -github.com/goreleaser/nfpm v1.10.3 h1:NzpWKKzSFr7JOn55XN0SskyFOjP6BkvRt3JujoX8fws= -github.com/goreleaser/nfpm v1.10.3/go.mod h1:EEC7YD5wi+ol0MiAshpgPANBOkjXDl7wqTLVk68OBsk= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= -github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= -github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= -github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q= -github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= -github.com/gostaticanalysis/forcetypeassert v0.1.0 h1:6eUflI3DiGusXGK6X7cCcIgVCpZ2CiZ1Q7jl6ZxNV70= -github.com/gostaticanalysis/forcetypeassert v0.1.0/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= -github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= -github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= -github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= -github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= -github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= -github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc= -github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= -github.com/graphql-go/handler v0.2.3 h1:CANh8WPnl5M9uA25c2GBhPqJhE53Fg0Iue/fRNla71E= -github.com/graphql-go/handler v0.2.3/go.mod h1:leLF6RpV5uZMN1CdImAxuiayrYYhOk33bZciaUGaXeU= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo= github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q= -github.com/hanwen/go-fuse/v2 v2.3.0 h1:t5ivNIH2PK+zw4OBul/iJjsoG9K6kXo4nMDoBpciC8A= -github.com/hanwen/go-fuse/v2 v2.3.0/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= -github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU= -github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU= -github.com/hashicorp/consul/api v1.24.0 h1:u2XyStA2j0jnCiVUU7Qyrt8idjRn4ORhK6DlvZ3bWhA= -github.com/hashicorp/consul/api v1.24.0/go.mod h1:NZJGRFYruc/80wYowkPFCp1LbGmJC9L8izrwfyVx/Wg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -2180,8 +1352,6 @@ github.com/hashicorp/go-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgM github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= @@ -2191,8 +1361,6 @@ github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b h1:3GrpnZQBxcMj1 github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b/go.mod h1:qIFzeFcJU3OIFk/7JreWXcUjFmcCaeHTH9KoNyHYVCs= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= -github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= @@ -2211,7 +1379,6 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= -github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= @@ -2222,8 +1389,6 @@ github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3q github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= -github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= @@ -2238,10 +1403,6 @@ github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwD github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= -github.com/hashicorp/vault/api v1.9.2 h1:YjkZLJ7K3inKgMZ0wzCU9OHqc+UqMQyXsPXnf3Cl2as= -github.com/hashicorp/vault/api v1.9.2/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= -github.com/hashicorp/vault/sdk v0.9.2 h1:H1kitfl1rG2SHbeGEyvhEqmIjVKE3E6c2q3ViKOs6HA= -github.com/hashicorp/vault/sdk v0.9.2/go.mod h1:gG0lA7P++KefplzvcD3vrfCmgxVAM7Z/SqX5NeOL/98= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU= @@ -2250,91 +1411,24 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/hugelgupf/go-shlex v0.0.0-20200702092117-c80c9d0918fa h1:s3KPo0nThtvjEamF/aElD4k5jSsBHew3/sgNTnth+2M= -github.com/hugelgupf/go-shlex v0.0.0-20200702092117-c80c9d0918fa/go.mod h1:I1uW6ymzwsy5TlQgD1bFAghdMgBYqH1qtCeHoZgHMqs= -github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= -github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1 h1:jWoR2Yqg8tzM0v6LAiP7i1bikZJu3gxpgvu3g1Lw+a0= github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= -github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= -github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= -github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= -github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= -github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= -github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c h1:gYfYE403/nlrGNYj6BEOs9ucLCAGB9gstlSk92DttTg= -github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= -github.com/intel-go/cpuid v0.0.0-20200819041909-2aa72927c3e2 h1:h+RKaNPjka7LRJGoeub/IQBdXSoEaJjfADkBq02hvjw= -github.com/intel-go/cpuid v0.0.0-20200819041909-2aa72927c3e2/go.mod h1:RmeVYf9XrPRbRc3XIx0gLYA8qOFvNoPOfaEZduRlEp4= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= -github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= -github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= -github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jgautheron/goconst v1.5.1 h1:HxVbL1MhydKs8R8n/HE5NPvzfaYmQJA3o879lE4+WcM= -github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= -github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= -github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= -github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -2346,34 +1440,16 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= -github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA= github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= -github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/junk1tm/musttag v0.5.0 h1:bV1DTdi38Hi4pG4OVWa7Kap0hi0o7EczuK6wQt9zPOM= -github.com/junk1tm/musttag v0.5.0/go.mod h1:PcR7BA+oREQYvHwgjIDmw3exJeds5JzRcvEJTfjrA0M= github.com/justinas/nosurf v1.2.0 h1:yMs1bSRrNiwXk4AS6n8vL2Ssgpb9CB25T/4xrixaK0s= github.com/justinas/nosurf v1.2.0/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= -github.com/k0kubun/pp v2.3.0+incompatible h1:EKhKbi34VQDWJtq+zpsKSEhkHHs9w2P8Izbq8IhLVSo= -github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= -github.com/kaey/framebuffer v0.0.0-20140402104929-7b385489a1ff h1:eK9dwGbaOkNQ44GPFD571d+51Qa2AGIYAR3kpjsrhLE= -github.com/kaey/framebuffer v0.0.0-20140402104929-7b385489a1ff/go.mod h1:tS4qtlcKqtt3tCIHUflVSqeP3CLH5Qtv2szX9X2SyhU= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= -github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -2381,13 +1457,7 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/errcheck v1.6.3 h1:dEKh+GLHcWm2oN34nMvDzn1sqI0i0WxPvrgiJA5JuM8= -github.com/kisielk/errcheck v1.6.3/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8= -github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg= -github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= @@ -2396,22 +1466,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= -github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg= -github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8= -github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 h1:dWzdsqjh1p2gNtRKqNwuBvKqMNwnLOPLzVZT1n6DK7s= -github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23/go.mod h1:lUaIXCWzf7BRKTY5iEcrYy1TfgbYLYVIS/B2vPkJzOc= -github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 h1:aC6MEAs3PE3lWD7lqrJfDxHd6hcced9R4JTZu85cJwU= -github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= -github.com/knqyf263/go-rpmdb v0.1.1 h1:oh68mTCvp1XzxdU7EfafcWzzfstUZAEa3MW0IJye584= -github.com/knqyf263/go-rpmdb v0.1.1/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww= -github.com/knqyf263/labeler v0.0.0-20200423181506-7a6e545148c3 h1:AvRd4VDhlo8/opGjS79zaVmIfoZzEOtBV1PvSRUkC7o= -github.com/knqyf263/labeler v0.0.0-20200423181506-7a6e545148c3/go.mod h1:DfoJpLAw0HeB4cYJFg1S8LqtYDvSKv3rh3wGAFwv5Bg= -github.com/knqyf263/nested v0.0.1 h1:Sv26CegUMhjt19zqbBKntjwESdxe5hxVPSk0+AKjdUc= -github.com/knqyf263/nested v0.0.1/go.mod h1:zwhsIhMkBg90DTOJQvxPkKIypEHPYkgWHs4gybdlUmk= -github.com/knz/bubbline v0.0.0-20230717192058-486954f9953f h1:Jjq9RwAsihYCm5fudPBj5CwsLDNd6r1ee3M2mfKFYdI= -github.com/knz/bubbline v0.0.0-20230717192058-486954f9953f/go.mod h1:ucXvyrucVy4jp/4afdKWNW1TVO73GMI72VNINzyT678= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -2421,117 +1475,43 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ktrysmt/go-bitbucket v0.6.4 h1:C8dUGp0qkwncKtAnozHCbbqhptefzEd1I0sfnuy9rYQ= -github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= -github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= -github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= -github.com/kunwardeep/paralleltest v1.0.6 h1:FCKYMF1OF2+RveWlABsdnmsvJrei5aoyZoaGS+Ugg8g= -github.com/kunwardeep/paralleltest v1.0.6/go.mod h1:Y0Y0XISdZM5IKm3TREQMZ6iteqn1YuwCsJO/0kL9Zes= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3 h1:Z9/bo5PSeMutpdiKYNt/TTSfGM1Ll0naj3QzYX9VxTc= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= -github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA= github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8 h1:Y7O3Z3YeNRtw14QrtHpevU4dSjCkov0J40MtQ7Nc0n8= github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8/go.mod h1:n/KX1BZoN1m9EwoXkn/xAV4fd3k8c++gGBsgLONaPOY= github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e h1:OP0ZMFeZkUnOzTFRfpuK3m7Kp4fNvC6qN+exwj7aI4M= github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= -github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= -github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= -github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= -github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= -github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA= -github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= -github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= -github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/leeavital/protoc-gen-gostreamer v0.1.0 h1:YYsSCUeNK4/0F69IkG916qYDAb4BxTH3kg8cC273cu4= -github.com/leeavital/protoc-gen-gostreamer v0.1.0/go.mod h1:sC19nxpNkHy3enGT3ck6LTr5mittUoUXE/elp/mnTS4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= -github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= -github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ= -github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= -github.com/liamg/iamgo v0.0.9 h1:tADGm3xVotyRJmuKKaH4+zsBn7LOcvgdpuF3WsSKW3c= -github.com/liamg/iamgo v0.0.9/go.mod h1:Kk6ZxBF/GQqG9nnaUjIi6jf+WXNpeOTyhwc6gnguaZQ= -github.com/liamg/jfather v0.0.7 h1:Xf78zS263yfT+xr2VSo6+kyAy4ROlCacRqJG7s5jt4k= -github.com/liamg/jfather v0.0.7/go.mod h1:xXBGiBoiZ6tmHhfy5Jzw8sugzajwYdi6VosIpB3/cPM= github.com/liamg/memoryfs v1.6.0 h1:jAFec2HI1PgMTem5gR7UT8zi9u4BfG5jorCRlLH06W8= github.com/liamg/memoryfs v1.6.0/go.mod h1:z7mfqXFQS8eSeBBsFjYLlxYRMRyiPktytvYCYTb3BSk= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= -github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI= github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= -github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= -github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= -github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= -github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= -github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= -github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= -github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= -github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= -github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= -github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= -github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= -github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0= github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA= github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4= github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= -github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMASY= -github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= -github.com/masahiro331/go-disk v0.0.0-20240625071113-56c933208fee h1:cgm8mE25x5XXX2oyvJDlyJ72K+rDu/4ZCYce2worNb8= -github.com/masahiro331/go-disk v0.0.0-20240625071113-56c933208fee/go.mod h1:rojbW5tVhH1cuVYFKZS+QX+VGXK45JVsRO+jW92kkKM= -github.com/masahiro331/go-ebs-file v0.0.0-20240917043618-e6d2bea5c32e h1:nCgF1JEYIS8KNuJtIeUrmjjhktIMKWNmASZqwK2ynu0= -github.com/masahiro331/go-ebs-file v0.0.0-20240917043618-e6d2bea5c32e/go.mod h1:XFWPTlAcEL733RUjbr0QBybdt6oK2DH7LZk8id2qtd4= -github.com/masahiro331/go-ext4-filesystem v0.0.0-20240620024024-ca14e6327bbd h1:JEIW94K3spsvBI5Xb9PGhKSIza9/jxO1lF30tPCAJlA= -github.com/masahiro331/go-ext4-filesystem v0.0.0-20240620024024-ca14e6327bbd/go.mod h1:3XMMY1M486mWGTD13WPItg6FsgflQR72ZMAkd+gsyoQ= -github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a h1:eLvAzVoRfHEOl64OxFhepPf3vj7SKvXY/tFc3BS0b7s= -github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a/go.mod h1:jZ3F25l7DbD7l7DcA8aj7eo1EZ84nbzcQHBB4lCSrI8= -github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd h1:Y30EzvuoVp97b0unb/GOFXzBUKRXZXUN2e0wYmvC+ic= -github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd/go.mod h1:5f7mCJGW9cJb8SDn3z8qodGxpMCOo8d/2nls/tiwRrw= -github.com/masahiro331/go-xfs-filesystem v0.0.0-20231205045356-1b22259a6c44 h1:VmSjn0UCyfXUNdePDr7uM/uZTnGSp+mKD5+cYkEoLx4= -github.com/masahiro331/go-xfs-filesystem v0.0.0-20231205045356-1b22259a6c44/go.mod h1:QKBZqdn6teT0LK3QhAf3K6xakItd1LonOShOEC44idQ= -github.com/masterminds/semver v1.5.0 h1:hTxJTTY7tjvnWMrl08O6u3G6BLlKVwxSz01lVac9P8U= -github.com/masterminds/semver v1.5.0/go.mod h1:s7KNT9fnd7edGzwwP7RBX4H0v/CYd5qdOLfkL1V75yg= -github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= -github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= -github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a h1:+J2gw7Bw77w/fbK7wnNJJDKmw1IbWft2Ul5BzrG1Qm8= -github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -2551,40 +1531,23 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo= -github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= -github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= -github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= -github.com/mgechev/revive v1.3.1 h1:OlQkcH40IB2cGuprTPcjB0iIUddgVZgGmDX3IAMR8D4= -github.com/mgechev/revive v1.3.1/go.mod h1:YlD6TTWl2B8A103R9KWJSPVI9DrEf+oqr15q21Ld+5I= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/microsoft/go-mssqldb v1.0.0 h1:k2p2uuG8T5T/7Hp7/e3vMGTnnR0sU4h8d1CcC71iLHU= -github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= -github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= -github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= -github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -2597,35 +1560,21 @@ github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= -github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/buildkit v0.21.1 h1:wTjVLfirh7skZt9piaIlNo8WdiPjza1CDl2EArDV9bA= -github.com/moby/buildkit v0.21.1/go.mod h1:mBq0D44uCyz2PdX8T/qym5LBbkBO3GGv0wqgX9ABYYw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= -github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/moby v28.2.2+incompatible h1:sBNZudYVackyiyn2yoBUpAoRcDun9bnUCozAW6lAnPs= github.com/moby/moby v28.2.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= -github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= -github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= -github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= @@ -2641,18 +1590,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= -github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= -github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= -github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm/Q= -github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= -github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -2665,60 +1604,24 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mutecomm/go-sqlcipher/v4 v4.4.0 h1:sV1tWCWGAVlPhNGT95Q+z/txFxuhAYWwHD1afF5bMZg= -github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= -github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= -github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= -github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= -github.com/nanmu42/limitio v1.0.0 h1:dpopBYPwUyLOPv+vsGja0iax+dG0SP9paTEmz+Sy7KU= -github.com/nanmu42/limitio v1.0.0/go.mod h1:8H40zQ7pqxzbwZ9jxsK2hDoE06TH5ziybtApt1io8So= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA= -github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba h1:fhFP5RliM2HW/8XdcO5QngSfFli9GcRIpMXvypTQt6E= -github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= -github.com/nishanths/exhaustive v0.10.0 h1:BMznKAcVa9WOoLq/kTGp4NJOJSMwEpcpjFNAVRfPlSo= -github.com/nishanths/exhaustive v0.10.0/go.mod h1:IbwrGdVMizvDcIxPYGVdQn5BqWJaOwpCvg4RGb8r/TA= -github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= -github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= -github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= -github.com/nunnatsa/ginkgolinter v0.11.2 h1:xzQpAsEyZe5F1RMy2Z5kn8UFCGiWfKqJOUd2ZzBXA4M= -github.com/nunnatsa/ginkgolinter v0.11.2/go.mod h1:dJIGXYXbkBswqa/pIzG0QlVTTDSBMxDoCFwhsl4Uras= -github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= -github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= -github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/open-policy-agent/opa v1.4.2 h1:ag4upP7zMsa4WE2p1pwAFeG4Pn3mNwfAx9DLhhJfbjU= github.com/open-policy-agent/opa v1.4.2/go.mod h1:DNzZPKqKh4U0n0ANxcCVlw8lCSv2c+h5G/3QvSYdWZ8= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 h1:lK/3zr73guK9apbXTcnDnYrC0YCQ25V3CIULYz3k2xU= @@ -2733,57 +1636,28 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= -github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= -github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= -github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/openvex/discovery v0.1.1-0.20240802171711-7c54efc57553 h1:c4u0GIH0w2Q57Pm2Oldrq6EiHFnLCCnRs98A+ggj/YQ= -github.com/openvex/discovery v0.1.1-0.20240802171711-7c54efc57553/go.mod h1:z4b//Qi7p7zcM/c41ogeTy+/nqfMbbeYnfZ+EMCTCD0= -github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ= -github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo= -github.com/orangecms/go-framebuffer v0.0.0-20200613202404-a0700d90c330 h1:zJBTzBuTR7EdFzmCGx0xp0pbOOb82sAh0+YUK4JTDEI= -github.com/orangecms/go-framebuffer v0.0.0-20200613202404-a0700d90c330/go.mod h1:3Myb/UszJY32F2G7yGkUtcW/ejHpjlGfYLim7cv2uKA= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0= github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= -github.com/owenrumney/go-sarif/v2 v2.3.3 h1:ubWDJcF5i3L/EIOER+ZyQ03IfplbSU1BLOE26uKQIIU= -github.com/owenrumney/go-sarif/v2 v2.3.3/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= -github.com/owenrumney/squealer v1.2.11 h1:vMudrj70VeOzY+t7Phz9Yo0wAgm4kXes9DcTLBVDqGY= -github.com/owenrumney/squealer v1.2.11/go.mod h1:8KOuitfOfmS/OtzgxQbxnnrbngAGopfgKB/BiGGpqGA= -github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= -github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= 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/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmOEA= -github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/peterbourgon/ff/v3 v3.3.0 h1:PaKe7GW8orVFh8Unb5jNHS+JZBwWUMa2se0HM6/BI24= -github.com/peterbourgon/ff/v3 v3.3.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= -github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= -github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= @@ -2801,8 +1675,6 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= -github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -2810,11 +1682,7 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/polyfloyd/go-errorlint v1.4.1 h1:r8ru5FhXSn34YU1GJDOuoJv2LdsQkPmK325EOpPMJlM= -github.com/polyfloyd/go-errorlint v1.4.1/go.mod h1:k6fU/+fQe38ednoZS51T7gSIGQW1y94d6TkSr35OzH8= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA= @@ -2832,26 +1700,10 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/quasilyte/go-ruleguard v0.3.19 h1:tfMnabXle/HzOb5Xe9CUZYWXKfkS1KwRmZyPmD9nVcc= -github.com/quasilyte/go-ruleguard v0.3.19/go.mod h1:lHSn69Scl48I7Gt9cX3VrbsZYvYiBYszZOZW4A+oTEw= github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= -github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= -github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= -github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= -github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= -github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= -github.com/rck/unit v0.0.3 h1:q3/Ui9gcrFKpEneZXw2gNmNEbzv5jLrZnH6qhX1ypZ0= -github.com/rck/unit v0.0.3/go.mod h1:jTOnzP4s1OjIP1vdxb4n76b23QPKS4EurYg7sYMr2DM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= -github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= -github.com/redis/rueidis v1.0.56 h1:DwPjFIgas1OMU/uCqBELOonu9TKMYt3MFPq6GtwEWNY= -github.com/redis/rueidis v1.0.56/go.mod h1:g660/008FMYmAF46HG4lmcpcgFNj+jCjCAZUUM+wEbs= -github.com/rekby/gpt v0.0.0-20200219180433-a930afbc6edc h1:goZGTwEEn8mWLcY012VouWZWkJ8GrXm9tS3VORMxT90= -github.com/rekby/gpt v0.0.0-20200219180433-a930afbc6edc/go.mod h1:scrOqOnnHVKCHENvFw8k9ajCb88uqLQDA4BvuJNJ2ew= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -2865,146 +1717,47 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79 h1:V7x0hCAgL8lNGezuex1RW1sh7VXXCqfw8nXZti66iFg= -github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg= -github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= -github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= -github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= -github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= -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/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c h1:8gOLsYwaY2JwlTMT4brS5/9XJdrdIbmk2obvQ748CC0= -github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c/go.mod h1:kwM/7r/rVluTE8qJbHAffduuqmSv4knVQT2IajGvSiA= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= -github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw= -github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= -github.com/ryanrolds/sqlclosecheck v0.4.0 h1:i8SX60Rppc1wRuyQjMciLqIzV3xnoHB7/tXbr6RGYNI= -github.com/ryanrolds/sqlclosecheck v0.4.0/go.mod h1:TBRRjzL31JONc9i4XMinicuo+s+E8yKZ5FN8X3G6CKQ= -github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/safchain/ethtool v0.0.0-20200218184317-f459e2d13664 h1:gvolwzuDhul9qK6/oHqxCHD5TEYfsWNBGidOeG6kvpk= -github.com/safchain/ethtool v0.0.0-20200218184317-f459e2d13664/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= -github.com/samber/oops v1.15.0 h1:/mF33KAqA2TugU6y/tomFpK6G6mJB7g0aqRyHkaSIeg= -github.com/samber/oops v1.15.0/go.mod h1:9LpLZkpjojEt/of7EpG5o65i/Lp23ddDvGhg2L871Ow= -github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= -github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= -github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= -github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= -github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= -github.com/sashamelentyev/usestdlibvars v1.23.0 h1:01h+/2Kd+NblNItNeux0veSL5cBF1jbEOPrEhDzGYq0= -github.com/sashamelentyev/usestdlibvars v1.23.0/go.mod h1:YPwr/Y1LATzHI93CqoPUN/2BzGQ/6N/cl/KwgR0B/aU= -github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= -github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= -github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= -github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc= -github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= -github.com/seccomp/libseccomp-golang v0.10.0 h1:aA4bp+/Zzi0BnWZ2F1wgNBs5gTpm+na2rWM6M9YjLpY= -github.com/seccomp/libseccomp-golang v0.10.0/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= -github.com/securego/gosec/v2 v2.15.0 h1:v4Ym7FF58/jlykYmmhZ7mTm7FQvN/setNm++0fgIAtw= -github.com/securego/gosec/v2 v2.15.0/go.mod h1:VOjTrZOkUtSDt2QLSJmQBMWnvwiQPEjg0l+5juIqGk8= -github.com/segmentio/kafka-go v0.4.42 h1:qffhBZCz4WcWyNuHEclHjIMLs2slp6mZO8px+5W5tfU= -github.com/segmentio/kafka-go v0.4.42/go.mod h1:d0g15xPMqoUookug0OU75DhGZxXwCFxSLeJ4uphwJzg= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= -github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= -github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= -github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= -github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= -github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk= github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sigstore/cosign/v2 v2.2.4 h1:iY4vtEacmu2hkNj1Fh+8EBqBwKs2DHM27/lbNWDFJro= -github.com/sigstore/cosign/v2 v2.2.4/go.mod h1:JZlRD2uaEjVAvZ1XJ3QkkZJhTqSDVtLaet+C/TMR81Y= -github.com/sigstore/protobuf-specs v0.4.1 h1:5SsMqZbdkcO/DNHudaxuCUEjj6x29tS2Xby1BxGU7Zc= -github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= -github.com/sigstore/rekor v1.3.10 h1:/mSvRo4MZ/59ECIlARhyykAlQlkmeAQpvBPlmJtZOCU= -github.com/sigstore/rekor v1.3.10/go.mod h1:JvryKJ40O0XA48MdzYUPu0y4fyvqt0C4iSY7ri9iu3A= -github.com/sigstore/sigstore v1.9.1 h1:bNMsfFATsMPaagcf+uppLk4C9rQZ2dh5ysmCxQBYWaw= -github.com/sigstore/sigstore v1.9.1/go.mod h1:zUoATYzR1J3rLNp3jmp4fzIJtWdhC3ZM6MnpcBtnsE4= -github.com/sigstore/timestamp-authority v1.2.2 h1:X4qyutnCQqJ0apMewFyx+3t7Tws00JQ/JonBiu3QvLE= -github.com/sigstore/timestamp-authority v1.2.2/go.mod h1:nEah4Eq4wpliDjlY342rXclGSO7Kb9hoRrl9tqLW13A= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= -github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/sivchari/nosnakecase v1.7.0 h1:7QkpWIRMe8x25gckkFd2A5Pi6Ymo0qgr4JrhGt95do8= -github.com/sivchari/nosnakecase v1.7.0/go.mod h1:CwDzrzPea40/GB6uynrNLiorAlgFRvRbFSgJx2Gs+QY= -github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= -github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/snowflakedb/gosnowflake v1.6.19 h1:KSHXrQ5o7uso25hNIzi/RObXtnSGkFgie91X82KcvMY= -github.com/snowflakedb/gosnowflake v1.6.19/go.mod h1:FM1+PWUdwB9udFDsXdfD58NONC0m+MlOSmQRvimobSM= -github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= -github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= github.com/sosedoff/gitkit v0.4.0 h1:opyQJ/h9xMRLsz2ca/2CRXtstePcpldiZN8DpLLF8Os= github.com/sosedoff/gitkit v0.4.0/go.mod h1:V3EpGZ0nvCBhXerPsbDeqtyReNb48cwP9KtkUYTKT5I= -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/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= -github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= -github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/fsync v0.10.1 h1:JRnB7G72b+gIBaBcpn5ibJSd7ww1iEahXSX2B8G6dSE= -github.com/spf13/fsync v0.10.1/go.mod h1:y+B41vYq5i6Boa3Z+BVoPbDeOvxVkNU5OBXhoT8i4TQ= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= -github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= -github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= -github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= -github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -3025,8 +1778,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= @@ -3035,40 +1786,22 @@ github.com/swaggo/http-swagger/v2 v2.0.1 h1:mNOBLxDjSNwCKlMxcErjjvct/xhc9t2KIO48 github.com/swaggo/http-swagger/v2 v2.0.1/go.mod h1:XYhrQVIKz13CxuKD4p4kvpaRB4jJ1/MlfQXVOE+CX8Y= github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= -github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= -github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= -github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d h1:K3j02b5j2Iw1xoggN9B2DIEkhWGheqFOeDkdJdBrJI8= github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs= -github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HPjrSuJYEkdZ+0ItmGQAQ75cRHIiftIyE= -github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= -github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns= -github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ= github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ= github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e/go.mod h1:DjoeCULdP6vTJ/xY+nzzR9LaUHprkbZEpNidX0aqEEk= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= -github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= -github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= -github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 h1:7xU7AFQE83h0wz/dIMvD0t77g0FxFfZIQjghDQxyG2U= -github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89/go.mod h1:OGMqrTzDqmJkGumUTtOv44Rp3/4xS+QFbE8Rn0AGlaU= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= -github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= -github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= -github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= -github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw= github.com/tdewolff/minify/v2 v2.20.37/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU= github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw= @@ -3080,116 +1813,44 @@ github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= github.com/testcontainers/testcontainers-go/modules/localstack v0.37.0 h1:nPuxUYseqS0eYJg7KDJd95PhoMhdpTnSNtkDLwWFngo= github.com/testcontainers/testcontainers-go/modules/localstack v0.37.0/go.mod h1:Mw+N4qqJ5iWbg45yWsdLzICfeCEwvYNudfAHHFqCU8Q= -github.com/tetafro/godot v1.4.11 h1:BVoBIqAf/2QdbFmSwAWnaIqDivZdOV0ZRwEm6jivLKw= -github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= -github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= -github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= -github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= -github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA= -github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= -github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= -github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= -github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= -github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= -github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= -github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= -github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= -github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= -github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= -github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= -github.com/tomarrell/wrapcheck/v2 v2.8.1 h1:HxSqDSN0sAt0yJYsrcYVoEeyM4aI9yAm3KQpIXDJRhQ= -github.com/tomarrell/wrapcheck/v2 v2.8.1/go.mod h1:/n2Q3NZ4XFT50ho6Hbxg+RV1uyo2Uow/Vdm9NQcl5SE= -github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= -github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= -github.com/tonglil/versioning v0.0.0-20170205083536-8b2a4334bd1d h1:3H+wrTJTy3PVEeCyrjiCWjrh7pVEodGgJgA8Q1tpcbg= -github.com/tonglil/versioning v0.0.0-20170205083536-8b2a4334bd1d/go.mod h1:/jU0OcDkhtRrbaJPiG/p3X7XOP1pkFWLvUbsnQKP6hY= -github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= -github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= -github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= -github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= -github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= -github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= -github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= -github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a h1:eg5FkNoQp76ZsswyGZ+TjYqA/rhKefxK8BW7XOlQsxo= github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a/go.mod h1:e/8TmrdreH0sZOw2DFKBaUV7bvDWRq6SeM9PzkuVM68= -github.com/u-root/iscsinl v0.1.1-0.20210528121423-84c32645822a h1:A0sK7WEodak7eVd21MOEatnh2pfAAwZaEPSIEEsjctQ= -github.com/u-root/iscsinl v0.1.1-0.20210528121423-84c32645822a/go.mod h1:RWIgJWqm9/0gjBZ0Hl8iR6MVGzZ+yAda2uqqLmetE2I= -github.com/u-root/mkuimage v0.0.0-20240225063926-11a3bcc79c2a h1:PsI71z55OoumrXP01sYr+cV3Ab4pL7Y2QW/ftMKG7CY= -github.com/u-root/mkuimage v0.0.0-20240225063926-11a3bcc79c2a/go.mod h1:Yqr8aXRStz71Z1JVz2bUut8Xt9wmBijQIVOSn3eYEIw= github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA= -github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= -github.com/ultraware/whitespace v0.0.5 h1:hh+/cpIcopyMYbZNVov9iSxvJU3OYQg78Sfaqzi/CzI= -github.com/ultraware/whitespace v0.0.5/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= -github.com/uptrace/bun v1.1.17 h1:qxBaEIo0hC/8O3O6GrMDKxqyT+mw5/s0Pn/n6xjyGIk= -github.com/uptrace/bun v1.1.17/go.mod h1:hATAzivtTIRsSJR4B8AXR+uABqnQxr3myKDKEf5iQ9U= -github.com/uptrace/bun/dialect/sqlitedialect v1.1.17 h1:i8NFU9r8YuavNFaYlNqi4ppn+MgoHtqLgpWQDrVTjm0= -github.com/uptrace/bun/dialect/sqlitedialect v1.1.17/go.mod h1:YF0FO4VVnY9GHNH6rM4r3STlVEBxkOc6L88Bm5X5mzA= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= -github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= -github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= -github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/uudashr/gocognit v1.0.6 h1:2Cgi6MweCsdB6kpcVQp7EW4U23iBFQWfTXiWlyp842Y= -github.com/uudashr/gocognit v1.0.6/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY= -github.com/valkey-io/valkey-go v1.0.56 h1:7qp/9dqqPbYEEKeFZCnpX6nzM5XzO2MPp0iKh9+c9Wg= -github.com/valkey-io/valkey-go v1.0.56/go.mod h1:sxpCChk8i3oTG+A/lUi9Lj8C/7WI+yhnQCvDJlPVKNM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= -github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= -github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8= -github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= -github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -3201,24 +1862,13 @@ github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vb github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/vtolstov/go-ioctl v0.0.0-20151206205506-6be9cced4810 h1:X6ps8XHfpQjw8dUStzlMi2ybiKQ2Fmdw7UM+TinwvyM= -github.com/vtolstov/go-ioctl v0.0.0-20151206205506-6be9cced4810/go.mod h1:dF0BBJ2YrV1+2eAIyEI+KeSidgA6HqoIP1u5XTlMq/o= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= -github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= -github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -3226,28 +1876,16 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -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/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= -github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= -github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= -github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= -github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= -github.com/yeya24/promlinter v0.2.0/go.mod h1:u54lkmBOZrpEbQQ6gox2zWKKLKu2SGe+2KOiextY+IA= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= @@ -3263,8 +1901,6 @@ github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= @@ -3279,20 +1915,6 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= -github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/bosi/decorder v0.2.3 h1:gX4/RgK16ijY8V+BRQHAySfQAb354T7/xQpDB2n10P0= -gitlab.com/bosi/decorder v0.2.3/go.mod h1:9K1RB5+VPNQYtXtTDAzd2OEftsZb1oV0IrJrzChSdGE= -gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= -gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= -gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= -gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= -go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= -go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M= -go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= -go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= -go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= -go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.nhat.io/otelsql v0.15.0 h1:e2lpIaFPe62Pa1fXZoOWXTvMzcN4SwHwHdCz1wDUG6c= @@ -3305,8 +1927,6 @@ go.opentelemetry.io/collector/component/componentstatus v0.120.0 h1:hzKjI9+AIl8A go.opentelemetry.io/collector/component/componentstatus v0.120.0/go.mod h1:kbuAEddxvcyjGLXGmys3nckAj4jTGC0IqDIEXAOr3Ag= go.opentelemetry.io/collector/component/componenttest v0.120.0 h1:vKX85d3lpxj/RoiFQNvmIpX9lOS80FY5svzOYUyeYX0= go.opentelemetry.io/collector/component/componenttest v0.120.0/go.mod h1:QDLboWF2akEqAGyvje8Hc7GfXcrZvQ5FhmlWvD5SkzY= -go.opentelemetry.io/collector/config/configtelemetry v0.119.0 h1:gAgMUEVXZKgpASxOrhS55DyA/aYatq0U6gitZI8MLXw= -go.opentelemetry.io/collector/config/configtelemetry v0.119.0/go.mod h1:SlBEwQg0qly75rXZ6W1Ig8jN25KBVBkFIIAUI1GiAAE= go.opentelemetry.io/collector/consumer v1.26.0 h1:0MwuzkWFLOm13qJvwW85QkoavnGpR4ZObqCs9g1XAvk= go.opentelemetry.io/collector/consumer v1.26.0/go.mod h1:I/ZwlWM0sbFLhbStpDOeimjtMbWpMFSoGdVmzYxLGDg= go.opentelemetry.io/collector/consumer/consumertest v0.120.0 h1:iPFmXygDsDOjqwdQ6YZcTmpiJeQDJX+nHvrjTPsUuv4= @@ -3366,13 +1986,9 @@ go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= -go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29 h1:w0QrHuh0hhUZ++UTQaBM2DMdrWQghZ/UsUb+Wb1+8YE= go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= @@ -3385,10 +2001,6 @@ go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 h1:X66ZEoMN2SuaoI/dfZVYobB6E5zjZyyHUMWlCA7MgGE= go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y= -gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng= -gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ= -golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= -golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -3423,8 +2035,6 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= -golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -3451,10 +2061,8 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= @@ -3695,8 +2303,6 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0= -golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -3814,8 +2420,6 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= -golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= -golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -3828,24 +2432,17 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= -golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= -gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= -gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -4050,8 +2647,6 @@ google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2Z google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20250425173222-7b384671a197 h1:km2eaPKRd1ganBNsKcAd9YOCLvTBQe8iTaPhE2q9n24= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20250425173222-7b384671a197/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 h1:29cjnHVylHwTzH66WfFZqgSQgnxzvWE+jvBwpZCLRxY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -4098,10 +2693,6 @@ google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpX google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= -google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= -google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -4129,31 +2720,13 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= -gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/jinzhu/gorm.v1 v1.9.2 h1:sTqyEcgrxG68jdeUXA9syQHNdeRhhfaYZ+vcL3x730I= -gopkg.in/jinzhu/gorm.v1 v1.9.2/go.mod h1:56JJPUzbikvTVnoyP1nppSkbJ2L8sunqTBDY2fDrmFg= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/olivere/elastic.v3 v3.0.75 h1:u3B8p1VlHF3yNLVOlhIWFT3F1ICcHfM5V6FFJe6pPSo= -gopkg.in/olivere/elastic.v3 v3.0.75/go.mod h1:yDEuSnrM51Pc8dM5ov7U8aI/ToR3PG0llA8aRv2qmw0= -gopkg.in/olivere/elastic.v5 v5.0.84 h1:acF/tRSg5geZpE3rqLglkS79CQMIMzOpWZE7hRXIkjs= -gopkg.in/olivere/elastic.v5 v5.0.84/go.mod h1:LXF6q9XNBxpMqrcgax95C6xyARXWbbCXUrtTxrNrxJI= -gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= -gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -4163,22 +2736,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw= -gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw= -gorm.io/driver/postgres v1.4.6 h1:1FPESNXqIKG5JmraaH2bfCVlMQ7paLoCreFxDtqzwdc= -gorm.io/driver/postgres v1.4.6/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4= -gorm.io/driver/sqlserver v1.4.2 h1:nMtEeKqv2R/vv9FoHUFWfXfP6SskAgRar0TPlZV1stk= -gorm.io/driver/sqlserver v1.4.2/go.mod h1:XHwBuB4Tlh7DqO0x7Ema8dmyWsQW7wi38VQOAFkrbXY= -gorm.io/gorm v1.25.3 h1:zi4rHZj1anhZS2EuEODMhDisGy+Daq9jtPrNGgbQYD8= -gorm.io/gorm v1.25.3/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc h1:DXLLFYv/k/xr0rWcwVEvWme1GR36Oc4kNMspg38JeiE= gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= -helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= -helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -4187,30 +2750,10 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= -honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= -k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= -k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= -k8s.io/cli-runtime v0.33.0 h1:Lbl/pq/1o8BaIuyn+aVLdEPHVN665tBAXUePs8wjX7c= -k8s.io/cli-runtime v0.33.0/go.mod h1:QcA+r43HeUM9jXFJx7A+yiTPfCooau/iCcP1wQh4NFw= -k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= -k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= -k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= -k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/kubectl v0.33.0 h1:HiRb1yqibBSCqic4pRZP+viiOBAnIdwYDpzUFejs07g= -k8s.io/kubectl v0.33.0/go.mod h1:gAlGBuS1Jq1fYZ9AjGWbI/5Vk3M/VW2DK4g10Fpyn/0= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= kernel.org/pub/linux/libs/security/libcap/cap v1.2.73 h1:Th2b8jljYqkyZKS3aD3N9VpYsQpHuXLgea+SZUIfODA= @@ -4219,39 +2762,17 @@ kernel.org/pub/linux/libs/security/libcap/psx v1.2.73 h1:SEAEUiPVylTD4vqqi+vtGkS kernel.org/pub/linux/libs/security/libcap/psx v1.2.73/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= -lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= -mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= -modernc.org/b v1.0.0 h1:vpvqeyp17ddcQWF29Czawql4lDdABCDRbXRAS4+aF2o= -modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= -modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= -modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0= -modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= -modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/db v1.0.0 h1:2c6NdCfaLnshSvY7OU09cyAY0gYXUZj4lmg5ItHyucg= -modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= -modernc.org/file v1.0.0 h1:9/PdvjVxd5+LcWUQIfapAWRGOkDLK90rloa8s/au06A= -modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= -modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= -modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= -modernc.org/golex v1.0.0 h1:wWpDlbK8ejRfSyi0frMyhilD3JBvtcx2AdGDnU+JtsE= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/internal v1.0.0 h1:XMDsFDcBDsibbBnHB2xzljZ+B1yrOVLEFkKL2u15Glw= -modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= @@ -4261,8 +2782,6 @@ modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= -modernc.org/lldb v1.0.0 h1:6vjDJxQEfhlOLwl4bhpwIz00uyFK4EmSYcbwqwbynsc= -modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= @@ -4274,73 +2793,24 @@ modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/ql v1.0.0 h1:bIQ/trWNVjQPlinI6jdOQsi195SIturGo3mp5hsDqVU= -modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= -modernc.org/sortutil v1.1.0 h1:oP3U4uM+NT/qBQcbg/K2iqAX0Nx7B1b6YZtq3Gk/PjM= -modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= -modernc.org/zappy v1.0.0 h1:dPVaP+3ueIUv4guk8PuZ2wiUGcJ1WUVvIheeSSTD0yk= -modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= -mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= -mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= -mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw= -mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg= -mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 h1:VuJo4Mt0EVPychre4fNlDWDuE5AjXtPJpRUWqZDQhaI= -mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8/go.mod h1:Oh/d7dEtzsNHGOq1Cdv8aMm3KdKhVvPbRQcM8WFpBR8= -oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= -oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= -oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= -oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= -pack.ag/tftp v1.0.1-0.20181129014014-07909dfbde3c h1:4DHuGX0VtxRIyjXlVpcjSGEmZ7OnIK7Hvo+INnxI8yk= -pack.ag/tftp v1.0.1-0.20181129014014-07909dfbde3c/go.mod h1:N1Pyo5YG+K90XHoR2vfLPhpRuE8ziqbgMn/r/SghZas= -rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= -rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU= -sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/kind v0.19.0 h1:ZSUh6/kpab6fiowT6EqL4k8xSbedI2NWxyuUOtoPFe4= -sigs.k8s.io/kind v0.19.0/go.mod h1:aBlbxg08cauDgZ612shr017/rZwqd7AS563FvpWKPVs= -sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= -sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= -sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= -sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= -src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f h1:pjVeIo9Ba6K1Wy+rlwX91zT7A+xGEmxiNRBdN04gDTQ= -src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f/go.mod h1:kPbhv5+fBeUh85nET3wWhHGUaUQ64nZMJ8FwA5v5Olg= storj.io/drpc v0.0.33 h1:yCGZ26r66ZdMP0IcTYsj7WDAUIIjzXk6DJhbhvt9FHI= storj.io/drpc v0.0.33/go.mod h1:vR804UNzhBa49NOJ6HeLjd2H3MakC1j5Gv8bsOQT6N4= From 3d5e2b55333cdb5835719f4d75c2de47a517e639 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 9 Jun 2025 15:30:20 +0200 Subject: [PATCH 09/61] WIP, memory-efficient approach Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 2 +- aibridged/bridge.go | 295 +++++++++++++++++++------------ aibridged/proxy.go | 384 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 567 insertions(+), 114 deletions(-) create mode 100644 aibridged/proxy.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 04ec9d3d5457c..4cada5b1b66db 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -70,7 +70,7 @@ func New(rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) initConnectionCh: make(chan struct{}), } - bridge := NewBridge(httpAddr, daemon.client) + bridge := NewBridge(httpAddr, logger.Named("ai_bridge"), daemon.client) daemon.bridge = bridge go daemon.connect() diff --git a/aibridged/bridge.go b/aibridged/bridge.go index c688cb4ce3844..b6ac3a3edfee4 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -6,20 +6,20 @@ import ( "encoding/json" "fmt" "io" - "log/slog" "net" "net/http" "net/http/httputil" "net/url" "os" "strings" - "time" + "cdr.dev/slog" "github.com/anthropics/anthropic-sdk-go" ant_ssestream "github.com/anthropics/anthropic-sdk-go/packages/ssestream" - "github.com/charmbracelet/log" "github.com/openai/openai-go" + "github.com/openai/openai-go/packages/param" openai_ssestream "github.com/openai/openai-go/packages/ssestream" + "github.com/tidwall/gjson" "golang.org/x/xerrors" "github.com/coder/coder/v2/aibridged/proto" @@ -29,20 +29,10 @@ type Bridge struct { httpSrv *http.Server addr string clientFn func() (proto.DRPCAIBridgeDaemonClient, bool) + logger slog.Logger } -func NewBridge(addr string, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) *Bridge { - // TODO: remove this. - { - handler := log.NewWithOptions(os.Stderr, log.Options{ - ReportCaller: true, // Enable caller reporting for debuggability - ReportTimestamp: true, - TimeFormat: time.TimeOnly, - }) - handler.SetLevel(log.DebugLevel) - slog.SetDefault(slog.New(handler)) - } - +func NewBridge(addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) *Bridge { var bridge Bridge mux := &http.ServeMux{} @@ -57,10 +47,123 @@ func NewBridge(addr string, clientFn func() (proto.DRPCAIBridgeDaemonClient, boo bridge.httpSrv = srv bridge.clientFn = clientFn + bridge.logger = logger return &bridge } +// ChatCompletionNewParamsWrapper exists because the "stream" param is not included in openai.ChatCompletionNewParams. +type ChatCompletionNewParamsWrapper struct { + openai.ChatCompletionNewParams `json:""` + Stream bool `json:"stream,omitempty"` +} + +func (b ChatCompletionNewParamsWrapper) MarshalJSON() ([]byte, error) { + type shadow ChatCompletionNewParamsWrapper + return param.MarshalWithExtras(b, (*shadow)(&b), map[string]any{ + "stream": b.Stream, + }) +} + +func (b *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { + err := b.ChatCompletionNewParams.UnmarshalJSON(raw) + if err != nil { + return err + } + + in := gjson.ParseBytes(raw) + if stream := in.Get("stream"); stream.Exists() { + b.Stream = stream.Bool() + if b.Stream { + b.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{ + IncludeUsage: openai.Bool(true), // Always include usage when streaming. + } + } else { + b.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} + } + } else { + b.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} + } + + return nil +} + +//type SSERoundTripper struct { +// transport http.RoundTripper +//} +// +//func (s *SSERoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { +// // Use default transport if none specified +// transport := s.transport +// if transport == nil { +// transport = &http.Transport{ +// DisableCompression: true, +// ResponseHeaderTimeout: 0, // No timeout for SSE +// IdleConnTimeout: 300 * time.Second, +// } +// } +// +// // Modify request for SSE +// req.Header.Set("Cache-Control", "no-cache") +// req.Header.Set("Accept", "text/event-stream") +// +// resp, err := transport.RoundTrip(req) +// if err != nil { +// return resp, err +// } +// +// resp.Body = wrapResponseBody(resp.Body) +// +// //var buf bytes.Buffer +// //teeReader := io.TeeReader(resp.Body, &buf) +// ////out, err := io.ReadAll(teeReader) +// ////if err != nil { +// //// return nil, xerrors.Errorf("intercept stream: %w", err) +// ////} +// // +// //newResp := &http.Response{ +// // Body: io.NopCloser(bytes.NewBuffer(buf.Bytes())), +// // Header: resp.Header, +// //} +// // +// //stream := openai_ssestream.NewStream[openai.ChatCompletionChunk](openai_ssestream.NewDecoder(newResp), nil) +// // +// //var msg openai.ChatCompletionAccumulator +// //for stream.Next() { +// // chunk := stream.Current() +// // msg.AddChunk(chunk) +// // +// // fmt.Println(chunk) +// //} +// +// return resp, err +//} +// +//func wrapResponseBody(body io.ReadCloser) io.ReadCloser { +// pr, pw := io.Pipe() +// go func() { +// defer pw.Close() +// defer body.Close() +// +// var buf bytes.Buffer +// teeReader := io.TeeReader(pr, &buf) +// +// // Read the entire stream first +// streamData, err := io.ReadAll(teeReader) +// if err != nil { +// return +// } +// +// // Write the original data to the pipe for the client +// go func() { +// defer pw.Close() +// pw.Write(streamData) +// }() +// }() +// +// return pr +//} + func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { coderdClient, ok := b.clientFn() if !ok { @@ -75,116 +178,82 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { return } - proxy := httputil.NewSingleHostReverseProxy(target) - originalDirector := proxy.Director - proxy.Director = func(req *http.Request) { - originalDirector(req) - - // Add OpenAI-specific headers - if strings.TrimSpace(req.Header.Get("Authorization")) == "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("OPENAI_API_KEY"))) - } - - if req.Header.Get("Content-Type") == "" { - req.Header.Set("Content-Type", "application/json") - } - - req.Host = target.Host - req.URL.Scheme = target.Scheme - req.URL.Host = target.Host - - body, err := io.ReadAll(req.Body) - if err != nil { - http.Error(w, "could not ready request body", http.StatusBadRequest) - return - } - _ = req.Body.Close() - - var msg openai.ChatCompletionNewParams - err = json.NewDecoder(bytes.NewReader(body)).Decode(&msg) - if err != nil { - http.Error(w, "could not unmarshal request body", http.StatusBadRequest) - return - } - - // TODO: robustness - if len(msg.Messages) > 0 { - latest := msg.Messages[len(msg.Messages)-1] - if latest.OfUser != nil { - if latest.OfUser.Content.OfString.String() != "" { - _, _ = coderdClient.TrackUserPrompts(r.Context(), &proto.TrackUserPromptsRequest{ - Prompt: strings.TrimSpace(latest.OfUser.Content.OfString.String()), - }) - } else { - fmt.Println() + proxy, err := NewSSEProxyWithConfig(ProxyConfig{ + Target: target, + RequestInterceptFunc: func(req *http.Request, body []byte) error { + var msg ChatCompletionNewParamsWrapper + err = json.NewDecoder(bytes.NewReader(body)).Decode(&msg) + if err != nil { + http.Error(w, "could not unmarshal request body", http.StatusBadRequest) + return xerrors.Errorf("unmarshal request body: %w", err) + } + // TODO: robustness + if len(msg.Messages) > 0 { + latest := msg.Messages[len(msg.Messages)-1] + if latest.OfUser != nil { + if latest.OfUser.Content.OfString.String() != "" { + _, _ = coderdClient.TrackUserPrompts(r.Context(), &proto.TrackUserPromptsRequest{ + Prompt: strings.TrimSpace(latest.OfUser.Content.OfString.String()), + }) + } } } - } - - req.Body = io.NopCloser(bytes.NewReader(body)) - - fmt.Printf("Proxying %s request to: %s\n", req.Method, req.URL.String()) - } - proxy.ModifyResponse = func(response *http.Response) error { - body, err := io.ReadAll(response.Body) - if err != nil { - return xerrors.Errorf("read response body: %w", err) - } - if err = response.Body.Close(); err != nil { - return xerrors.Errorf("close body: %w", err) - } - - if !strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { - var msg openai.ChatCompletion + return nil + }, + ResponseInterceptFunc: func(data []byte, isStreaming bool) error { + b.logger.Info(r.Context(), "openai response received", slog.F("data", data), slog.F("streaming", isStreaming)) - // TODO: check content-encoding to handle others. - gr, err := gzip.NewReader(bytes.NewReader(body)) - if err != nil { - return xerrors.Errorf("parse gzip-encoded body: %w", err) + if !isStreaming { + return nil } - err = json.NewDecoder(gr).Decode(&msg) - if err != nil { - return xerrors.Errorf("parse non-streaming body: %w", err) + response := &http.Response{ + Body: io.NopCloser(bytes.NewReader(data)), + } + stream := openai_ssestream.NewStream[openai.ChatCompletionChunk](openai_ssestream.NewDecoder(response), nil) + + var ( + inputToks, outputToks int64 + ) + var msg openai.ChatCompletionAccumulator + for stream.Next() { + msg.AddChunk(stream.Current()) + b.logger.Info(r.Context(), "openai chunk", slog.F("msgID", msg.ID), slog.F("contents", fmt.Sprintf("%+v", msg))) + + if msg.Usage.PromptTokens+msg.Usage.CompletionTokens > 0 { + inputToks = msg.Usage.PromptTokens + outputToks = msg.Usage.CompletionTokens + } } - _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ - MsgId: msg.ID, - InputTokens: msg.Usage.PromptTokens, - OutputTokens: msg.Usage.CompletionTokens, - }) + if inputToks+outputToks > 0 { + _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ + MsgId: msg.ID, + InputTokens: inputToks, + OutputTokens: outputToks, + }) + } - response.Body = io.NopCloser(bytes.NewReader(body)) return nil - } - - response.Body = io.NopCloser(bytes.NewReader(body)) - stream := openai_ssestream.NewStream[openai.ChatCompletionChunk](openai_ssestream.NewDecoder(response), nil) - - var ( - inputToks, outputToks int64 - ) - - var msg openai.ChatCompletionAccumulator - for stream.Next() { - chunk := stream.Current() - msg.AddChunk(chunk) + }, + }) + if err != nil { + b.logger.Error(r.Context(), "failed to create OpenAI proxy", slog.Error(err)) + http.Error(w, "failed to create OpenAI proxy", http.StatusInternalServerError) + return + } - if msg.Usage.PromptTokens+msg.Usage.CompletionTokens > 0 { - inputToks = msg.Usage.PromptTokens - outputToks = msg.Usage.CompletionTokens - } + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) + //Add OpenAI-specific headers + if strings.TrimSpace(req.Header.Get("Authorization")) == "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("OPENAI_API_KEY"))) } + } - _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ - MsgId: msg.ID, - InputTokens: inputToks, - OutputTokens: outputToks, - }) - - response.Body = io.NopCloser(bytes.NewReader(body)) - - return nil + proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) } proxy.ServeHTTP(w, r) } diff --git a/aibridged/proxy.go b/aibridged/proxy.go new file mode 100644 index 0000000000000..b43a33c377858 --- /dev/null +++ b/aibridged/proxy.go @@ -0,0 +1,384 @@ +package aibridged + +import ( + "bufio" + "bytes" + "context" + "io" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "sync" + "time" + + "golang.org/x/xerrors" +) + +// ResponseInterceptFunc is called for each chunk of data received from the upstream server. +// For streaming responses, it's called for each SSE event or data chunk. +// For non-streaming responses, it's called once with the complete response body. +// The function receives a copy of the data - modifications don't affect the response. +type ResponseInterceptFunc func(data []byte, isStreaming bool) error + +// RequestInterceptFunc is called for each request before it's sent to the upstream server. +// The function receives a copy of the request body - modifications don't affect the request. +type RequestInterceptFunc func(req *http.Request, body []byte) error + +// RequestModifyFunc is called to modify the request body before sending to upstream. +// Unlike RequestInterceptFunc, this function can modify the request body. +// Returns the modified body or an error. +type RequestModifyFunc func(req *http.Request, body []byte) ([]byte, error) + +// SSEProxy provides a fast, memory-efficient proxy using httputil.ReverseProxy. +// It supports efficient copying of responses for interception without buffering. +type SSEProxy struct { + *httputil.ReverseProxy + responseInterceptFunc ResponseInterceptFunc + requestInterceptFunc RequestInterceptFunc + requestModifyFunc RequestModifyFunc + bufferPool sync.Pool +} + +// NewSSEProxy creates a new SSE proxy that proxies to the given target URL. +func NewSSEProxy(target *url.URL, responseInterceptFunc ResponseInterceptFunc) *SSEProxy { + return NewSSEProxyWithRequestIntercept(target, responseInterceptFunc, nil) +} + +// NewSSEProxyWithRequestIntercept creates a new SSE proxy with both request and response interception. +func NewSSEProxyWithRequestIntercept(target *url.URL, responseInterceptFunc ResponseInterceptFunc, requestInterceptFunc RequestInterceptFunc) *SSEProxy { + proxy := &SSEProxy{ + responseInterceptFunc: responseInterceptFunc, + requestInterceptFunc: requestInterceptFunc, + bufferPool: sync.Pool{ + New: func() interface{} { + // Use 32KB buffers for optimal performance + return make([]byte, 32*1024) + }, + }, + } + + proxy.ReverseProxy = httputil.NewSingleHostReverseProxy(target) + + // Configure for optimal streaming performance + proxy.FlushInterval = -1 // Immediate flushing + + // Custom transport for streaming optimization + proxy.Transport = &http.Transport{ + DisableCompression: true, // Disable compression for streaming + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + ResponseHeaderTimeout: 0, // No timeout for streaming responses + } + + // Custom director for request interception + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) + + // Intercept request if function is provided + if proxy.requestInterceptFunc != nil { + proxy.interceptRequest(req) + } + } + + // Custom response modifier for interception + originalModifyResponse := proxy.ModifyResponse + proxy.ModifyResponse = func(resp *http.Response) error { + // Call original modifier first if it exists + if originalModifyResponse != nil { + if err := originalModifyResponse(resp); err != nil { + return err + } + } + + // Wrap the response body for interception + resp.Body = proxy.wrapResponseBody(resp.Body, resp.Header) + return nil + } + + return proxy +} + +// interceptRequest intercepts and processes the request body +func (p *SSEProxy) interceptRequest(req *http.Request) { + if req.Body == nil { + // Call intercept function with empty body + if p.requestInterceptFunc != nil { + if err := p.requestInterceptFunc(req, nil); err != nil { + // Log error but continue + _ = err + } + } + return + } + + // Read the request body + body, err := io.ReadAll(req.Body) + if err != nil { + // Log error but continue + _ = err + return + } + + // Close the original body + req.Body.Close() + + // Create a copy for interception + bodyCopy := make([]byte, len(body)) + copy(bodyCopy, body) + + // Call intercept function if provided + if p.requestInterceptFunc != nil { + if err := p.requestInterceptFunc(req, bodyCopy); err != nil { + // Log error but continue + _ = err + } + } + + // Apply request modifications if provided + finalBody := body + if p.requestModifyFunc != nil { + modifiedBody, err := p.requestModifyFunc(req, bodyCopy) + if err != nil { + // Log error but continue with original body + _ = err + } else { + finalBody = modifiedBody + } + } + + // Restore the request body for the upstream request + req.Body = io.NopCloser(bytes.NewReader(finalBody)) + req.ContentLength = int64(len(finalBody)) +} + +// wrapResponseBody wraps the response body to enable interception +func (p *SSEProxy) wrapResponseBody(body io.ReadCloser, headers http.Header) io.ReadCloser { + if p.responseInterceptFunc == nil { + return body + } + + // Determine if this is a streaming response + contentType := headers.Get("Content-Type") + isStreaming := strings.Contains(contentType, "text/event-stream") || + strings.Contains(contentType, "text/plain") || + headers.Get("Transfer-Encoding") == "chunked" + + if isStreaming { + return &streamingInterceptReader{ + ReadCloser: body, + interceptFunc: p.responseInterceptFunc, + bufferPool: &p.bufferPool, + } + } + + return &nonStreamingInterceptReader{ + ReadCloser: body, + interceptFunc: p.responseInterceptFunc, + } +} + +// streamingInterceptReader intercepts streaming responses chunk by chunk +type streamingInterceptReader struct { + io.ReadCloser + interceptFunc ResponseInterceptFunc + bufferPool *sync.Pool + reader *bufio.Reader + readerOnce sync.Once +} + +func (sir *streamingInterceptReader) Read(p []byte) (n int, err error) { + // Initialize buffered reader once + sir.readerOnce.Do(func() { + sir.reader = bufio.NewReader(sir.ReadCloser) + }) + + n, err = sir.reader.Read(p) + if n > 0 { + // Call intercept function with a copy of the chunk + chunk := make([]byte, n) + copy(chunk, p[:n]) + + if interceptErr := sir.interceptFunc(chunk, true); interceptErr != nil { + // Log error but continue reading + _ = interceptErr + } + } + return n, err +} + +// nonStreamingInterceptReader intercepts complete non-streaming responses +type nonStreamingInterceptReader struct { + io.ReadCloser + interceptFunc ResponseInterceptFunc + buffer bytes.Buffer + intercepted bool + mu sync.Mutex +} + +func (nsir *nonStreamingInterceptReader) Read(p []byte) (n int, err error) { + n, err = nsir.ReadCloser.Read(p) + + if n > 0 { + nsir.mu.Lock() + nsir.buffer.Write(p[:n]) + nsir.mu.Unlock() + } + + // If we've reached EOF and haven't intercepted yet, do it now + if err == io.EOF && !nsir.intercepted { + nsir.mu.Lock() + if !nsir.intercepted { + nsir.intercepted = true + data := nsir.buffer.Bytes() + nsir.mu.Unlock() + + if interceptErr := nsir.interceptFunc(data, false); interceptErr != nil { + // Log error but continue + _ = interceptErr + } + } else { + nsir.mu.Unlock() + } + } + + return n, err +} + +// ProxyConfig holds configuration for the SSE proxy +type ProxyConfig struct { + Target *url.URL + ResponseInterceptFunc ResponseInterceptFunc + RequestInterceptFunc RequestInterceptFunc + RequestModifyFunc RequestModifyFunc + ModifyRequest func(*http.Request) + ModifyResponse func(*http.Response) error + ErrorHandler func(http.ResponseWriter, *http.Request, error) + Transport http.RoundTripper + FlushInterval time.Duration // -1 for immediate flushing +} + +// NewSSEProxyWithConfig creates a new SSE proxy with custom configuration +func NewSSEProxyWithConfig(config ProxyConfig) (*SSEProxy, error) { + if config.Target == nil { + return nil, xerrors.Errorf("target URL is required") + } + + proxy := &SSEProxy{ + responseInterceptFunc: config.ResponseInterceptFunc, + requestInterceptFunc: config.RequestInterceptFunc, + requestModifyFunc: config.RequestModifyFunc, + bufferPool: sync.Pool{ + New: func() interface{} { + return make([]byte, 32*1024) + }, + }, + } + + proxy.ReverseProxy = httputil.NewSingleHostReverseProxy(config.Target) + + // Configure flush interval + if config.FlushInterval != 0 { + proxy.FlushInterval = config.FlushInterval + } else { + proxy.FlushInterval = -1 // Default to immediate flushing + } + + // Configure transport + if config.Transport != nil { + proxy.Transport = config.Transport + } else { + proxy.Transport = &http.Transport{ + DisableCompression: true, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + ResponseHeaderTimeout: 0, + } + } + + // Configure request modifier and interception + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) + + req.Host = config.Target.Host + req.URL.Scheme = config.Target.Scheme + req.URL.Host = config.Target.Host + + // Apply custom request modifications first + if config.ModifyRequest != nil { + config.ModifyRequest(req) + } + + // Then apply request interception and modification + if proxy.requestInterceptFunc != nil || proxy.requestModifyFunc != nil { + proxy.interceptRequest(req) + } + } + + // Configure response modifier + proxy.ModifyResponse = func(resp *http.Response) error { + // Call custom modifier first if it exists + if config.ModifyResponse != nil { + if err := config.ModifyResponse(resp); err != nil { + return err + } + } + + // Wrap the response body for interception + resp.Body = proxy.wrapResponseBody(resp.Body, resp.Header) + return nil + } + + // Configure error handler + if config.ErrorHandler != nil { + proxy.ErrorHandler = config.ErrorHandler + } + + return proxy, nil +} + +// ServeHTTPWithContext serves HTTP requests with context for cancellation +func (p *SSEProxy) ServeHTTPWithContext(ctx context.Context, w http.ResponseWriter, r *http.Request) { + // Create a new request with the given context + r = r.WithContext(ctx) + + // Set up cancellation monitoring + done := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + // Context cancelled, try to close the connection + if hijacker, ok := w.(http.Hijacker); ok { + if conn, _, err := hijacker.Hijack(); err == nil { + conn.Close() + } + } + case <-done: + // Request completed normally + } + }() + + // Serve the request + p.ServeHTTP(w, r) + close(done) +} + +// NewGenericProxy creates a proxy for any target URL +func NewGenericProxy(targetURL string, responseInterceptFunc ResponseInterceptFunc) (*SSEProxy, error) { + target, err := url.Parse(targetURL) + if err != nil { + return nil, xerrors.Errorf("parse target URL: %w", err) + } + return NewSSEProxy(target, responseInterceptFunc), nil +} + +// NewGenericProxyWithRequestIntercept creates a proxy for any target URL with request interception +func NewGenericProxyWithRequestIntercept(targetURL string, responseInterceptFunc ResponseInterceptFunc, requestInterceptFunc RequestInterceptFunc) (*SSEProxy, error) { + target, err := url.Parse(targetURL) + if err != nil { + return nil, xerrors.Errorf("parse target URL: %w", err) + } + return NewSSEProxyWithRequestIntercept(target, responseInterceptFunc, requestInterceptFunc), nil +} From 24b54fb20d8abe7104f1ee0e5bfc9029ea4f6b2d Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 9 Jun 2025 18:05:22 +0200 Subject: [PATCH 10/61] WIP, tool calls Signed-off-by: Danny Kopping --- aibridged/bridge.go | 76 ++++++++++++++++++++++++++++++++++++++++----- aibridged/proxy.go | 18 ----------- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index b6ac3a3edfee4..399731c3672db 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -178,8 +178,52 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { return } + var acc openai.ChatCompletionAccumulator proxy, err := NewSSEProxyWithConfig(ProxyConfig{ Target: target, + ModifyRequest: func(req *http.Request) { + var in ChatCompletionNewParamsWrapper + + body, err := io.ReadAll(req.Body) + if err != nil { + b.logger.Error(req.Context(), "failed to read body", slog.Error(err)) + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + if err = json.Unmarshal(body, &in); err != nil { + b.logger.Error(req.Context(), "failed to unmarshal request", slog.Error(err)) + http.Error(w, "failed to unmarshal request", http.StatusInternalServerError) + return + } + + in.Tools = []openai.ChatCompletionToolParam{ + { + Function: openai.FunctionDefinitionParam{ + Name: "get_weather", + Description: openai.String("Get weather at the given location"), + Parameters: openai.FunctionParameters{ + "type": "object", + "properties": map[string]interface{}{ + "location": map[string]string{ + "type": "string", + }, + }, + "required": []string{"location"}, + }, + }, + }, + } + + newBody, err := json.Marshal(in) + if err != nil { + b.logger.Error(req.Context(), "failed to marshal request", slog.Error(err)) + http.Error(w, "failed to marshal request", http.StatusInternalServerError) + return + } + + req.Body = io.NopCloser(bytes.NewReader(newBody)) + }, RequestInterceptFunc: func(req *http.Request, body []byte) error { var msg ChatCompletionNewParamsWrapper err = json.NewDecoder(bytes.NewReader(body)).Decode(&msg) @@ -215,25 +259,43 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { var ( inputToks, outputToks int64 ) - var msg openai.ChatCompletionAccumulator for stream.Next() { - msg.AddChunk(stream.Current()) - b.logger.Info(r.Context(), "openai chunk", slog.F("msgID", msg.ID), slog.F("contents", fmt.Sprintf("%+v", msg))) + acc.AddChunk(stream.Current()) + b.logger.Info(r.Context(), "openai chunk", slog.F("msgID", acc.ID), slog.F("contents", fmt.Sprintf("%+v", acc))) - if msg.Usage.PromptTokens+msg.Usage.CompletionTokens > 0 { - inputToks = msg.Usage.PromptTokens - outputToks = msg.Usage.CompletionTokens + if acc.Usage.PromptTokens+acc.Usage.CompletionTokens > 0 { + inputToks = acc.Usage.PromptTokens + outputToks = acc.Usage.CompletionTokens } + + //for _, c := range msg.ChatCompletion.Choices { + // for _, t := range c.Message.ToolCalls { + // fmt.Println(t.Function.Name, t.Function.Arguments) + // } + //} + } + if err := stream.Err(); err != nil { + panic(err) } if inputToks+outputToks > 0 { _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ - MsgId: msg.ID, + MsgId: acc.ID, InputTokens: inputToks, OutputTokens: outputToks, }) } + if len(acc.Choices) < 0 { + return nil + } + + for _, c := range acc.Choices { + for _, t := range c.Message.ToolCalls { + // Now that we can execute tools and add new messages, we'll need to append these to the stream (?) + } + } + return nil }, }) diff --git a/aibridged/proxy.go b/aibridged/proxy.go index b43a33c377858..056d7aa1e90db 100644 --- a/aibridged/proxy.go +++ b/aibridged/proxy.go @@ -364,21 +364,3 @@ func (p *SSEProxy) ServeHTTPWithContext(ctx context.Context, w http.ResponseWrit p.ServeHTTP(w, r) close(done) } - -// NewGenericProxy creates a proxy for any target URL -func NewGenericProxy(targetURL string, responseInterceptFunc ResponseInterceptFunc) (*SSEProxy, error) { - target, err := url.Parse(targetURL) - if err != nil { - return nil, xerrors.Errorf("parse target URL: %w", err) - } - return NewSSEProxy(target, responseInterceptFunc), nil -} - -// NewGenericProxyWithRequestIntercept creates a proxy for any target URL with request interception -func NewGenericProxyWithRequestIntercept(targetURL string, responseInterceptFunc ResponseInterceptFunc, requestInterceptFunc RequestInterceptFunc) (*SSEProxy, error) { - target, err := url.Parse(targetURL) - if err != nil { - return nil, xerrors.Errorf("parse target URL: %w", err) - } - return NewSSEProxyWithRequestIntercept(target, responseInterceptFunc, requestInterceptFunc), nil -} From 7088d57c7b169c8a86ea0db5d17761783aa1973a Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 11 Jun 2025 05:37:48 +0200 Subject: [PATCH 11/61] WIP: CORS --- coderd/aibridge.go | 4 ++++ coderd/coderd.go | 4 +++- coderd/httpmw/cors.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/coderd/aibridge.go b/coderd/aibridge.go index f2333e081977c..8a2a1fa5d6311 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -14,6 +14,10 @@ import ( func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() + // Something, somewhere is adding a duplicate header. + // Haven't been able to track it down yet. + rw.Header().Del("Access-Control-Allow-Origin") + if len(api.AIBridgeDaemons) == 0 { http.Error(rw, "no AI bridge daemons running", http.StatusInternalServerError) return diff --git a/coderd/coderd.go b/coderd/coderd.go index d035c5dc9c5fa..8dd777c8e439e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -826,7 +826,9 @@ func New(options *Options) *API { expvar.Publish("derp", api.DERPServer.ExpVar()) } }) - cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value()) + regularCors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value()) + permissiveCors := httpmw.PermissiveCors() + cors := httpmw.ConditionalCors("/api/v2/aibridge", regularCors, permissiveCors) prometheusMW := httpmw.Prometheus(options.PrometheusRegistry) r.Use( diff --git a/coderd/httpmw/cors.go b/coderd/httpmw/cors.go index 2350a7dd3b8a6..b503b003e3d45 100644 --- a/coderd/httpmw/cors.go +++ b/coderd/httpmw/cors.go @@ -4,6 +4,7 @@ import ( "net/http" "net/url" "regexp" + "strings" "github.com/go-chi/cors" @@ -73,3 +74,37 @@ func WorkspaceAppCors(regex *regexp.Regexp, app appurl.ApplicationURL) func(next AllowCredentials: true, }) } + +// PermissiveCors creates a very permissive CORS middleware that allows all origins, +// methods, and headers. This bypasses go-chi's CORS library for maximum compatibility. +func PermissiveCors() func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH, SNARF") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Set("Access-Control-Max-Age", "86400") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) + } +} + +// ConditionalCors applies permissive CORS for requests with the specified prefix, +// and regular CORS for all other requests. +func ConditionalCors(prefix string, regularCors, permissiveCors func(next http.Handler) http.Handler) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, prefix) { + permissiveCors(next).ServeHTTP(w, r) + } else { + regularCors(next).ServeHTTP(w, r) + } + }) + } +} From 1c74d5fc2baaa415fc2b17fa57f81253daabb011 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 11 Jun 2025 05:38:47 +0200 Subject: [PATCH 12/61] WIP: /v1/models Signed-off-by: Danny Kopping --- coderd/coderd.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/coderd/coderd.go b/coderd/coderd.go index 8dd777c8e439e..072a58405d4a8 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1501,6 +1501,19 @@ func New(options *Options) *API { }) r.Route("/aibridge", func(r chi.Router) { r.Post("/v1/chat/completions", api.bridgeAIRequest) + r.Get("/v1/models", func(rw http.ResponseWriter, r *http.Request) { + httpapi.Write(context.Background(), rw, http.StatusOK, map[string]any{ + "object": "list", + "data": []map[string]any{ + { + "id": "gpt-4-0613", + "object": "model", + "created": 1686588896, + "owned_by": "openai", + }, + }, + }) + }) r.Post("/v1/messages", api.bridgeAIRequest) }) }) From e3d34004b6197d2fdfc0c9dc134c5aa843a22ac4 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 11 Jun 2025 11:06:20 +0200 Subject: [PATCH 13/61] WIP: starting on tests & logical sessions to handle tool calls Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 5 +- aibridged/bridge.go | 101 ++++++++++++++++++++++++-------- aibridged/bridge_test.go | 120 +++++++++++++++++++++++++++++++++++++++ aibridged/proxy.go | 45 +++++++++++---- aibridged/session.go | 50 ++++++++++++++++ cli/server.go | 6 +- codersdk/deployment.go | 33 +++++++++-- 7 files changed, 318 insertions(+), 42 deletions(-) create mode 100644 aibridged/bridge_test.go create mode 100644 aibridged/session.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 4cada5b1b66db..e71a65f9f5bc6 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -53,7 +53,7 @@ type Server struct { var _ proto.DRPCAIBridgeDaemonServer = &Server{} -func New(rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) { +func New(rpcDialer Dialer, httpAddr string, logger slog.Logger, bridgeCfg codersdk.AIBridgeConfig) (*Server, error) { if rpcDialer == nil { return nil, xerrors.Errorf("nil rpcDialer given") } @@ -70,9 +70,10 @@ func New(rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) initConnectionCh: make(chan struct{}), } - bridge := NewBridge(httpAddr, logger.Named("ai_bridge"), daemon.client) + bridge := NewBridge(bridgeCfg, httpAddr, logger.Named("ai_bridge"), daemon.client) daemon.bridge = bridge + daemon.wg.Add(1) go daemon.connect() go func() { err := bridge.Serve() diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 399731c3672db..b3cff9757fc0e 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -23,16 +23,19 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/aibridged/proto" + "github.com/coder/coder/v2/codersdk" ) type Bridge struct { + cfg codersdk.AIBridgeConfig + httpSrv *http.Server addr string clientFn func() (proto.DRPCAIBridgeDaemonClient, bool) logger slog.Logger } -func NewBridge(addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) *Bridge { +func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) *Bridge { var bridge Bridge mux := &http.ServeMux{} @@ -45,6 +48,7 @@ func NewBridge(addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBri // TODO: other settings. } + bridge.cfg = cfg bridge.httpSrv = srv bridge.clientFn = clientFn bridge.logger = logger @@ -164,6 +168,16 @@ func (b *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { // return pr //} +func (b *Bridge) openAITarget() *url.URL { + u := b.cfg.OpenAIBaseURL.String() + target, err := url.Parse(u) + if err != nil { + panic(fmt.Sprintf("failed to parse %q", u)) + return nil + } + return target +} + func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { coderdClient, ok := b.clientFn() if !ok { @@ -172,15 +186,10 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { return } - target, err := url.Parse("https://api.openai.com") - if err != nil { - http.Error(w, "failed to parse OpenAI URL", http.StatusInternalServerError) - return - } - var acc openai.ChatCompletionAccumulator proxy, err := NewSSEProxyWithConfig(ProxyConfig{ - Target: target, + OpenAISession: NewOpenAISession(), + Target: b.openAITarget(), ModifyRequest: func(req *http.Request) { var in ChatCompletionNewParamsWrapper @@ -226,7 +235,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { }, RequestInterceptFunc: func(req *http.Request, body []byte) error { var msg ChatCompletionNewParamsWrapper - err = json.NewDecoder(bytes.NewReader(body)).Decode(&msg) + err := json.NewDecoder(bytes.NewReader(body)).Decode(&msg) if err != nil { http.Error(w, "could not unmarshal request body", http.StatusBadRequest) return xerrors.Errorf("unmarshal request body: %w", err) @@ -244,11 +253,11 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } return nil }, - ResponseInterceptFunc: func(data []byte, isStreaming bool) error { + ResponseInterceptFunc: func(session *OpenAISession, data []byte, isStreaming bool) ([][]byte, bool, error) { b.logger.Info(r.Context(), "openai response received", slog.F("data", data), slog.F("streaming", isStreaming)) if !isStreaming { - return nil + return nil, true, nil } response := &http.Response{ @@ -260,7 +269,9 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { inputToks, outputToks int64 ) for stream.Next() { - acc.AddChunk(stream.Current()) + chunk := stream.Current() + + acc.AddChunk(chunk) b.logger.Info(r.Context(), "openai chunk", slog.F("msgID", acc.ID), slog.F("contents", fmt.Sprintf("%+v", acc))) if acc.Usage.PromptTokens+acc.Usage.CompletionTokens > 0 { @@ -268,11 +279,48 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { outputToks = acc.Usage.CompletionTokens } - //for _, c := range msg.ChatCompletion.Choices { - // for _, t := range c.Message.ToolCalls { - // fmt.Println(t.Function.Name, t.Function.Arguments) - // } - //} + var foundToolCallDelta bool + for _, c := range chunk.Choices { + for range c.Delta.ToolCalls { + foundToolCallDelta = true + + // Grab values from accumulator instead of delta. + for _, ac := range acc.ChatCompletion.Choices { + for _, at := range ac.Message.ToolCalls { + var ( + tc *OpenAIToolCall + ok bool + ) + if tc, ok = session.toolCallsRequired[at.ID]; !ok { + session.toolCallsRequired[at.ID] = &OpenAIToolCall{} + tc = session.toolCallsRequired[at.ID] + } + + session.toolCallsState[at.ID] = OpenAIToolCallNotReady + + tc.funcName = at.Function.Name + args := make(map[string]string) + err := json.Unmarshal([]byte(at.Function.Arguments), &args) + if err == nil { // Note: inverted. + tc.args = args + } + } + } + } + + // Once we receive a finish reason of "tool_calls", the API is waiting for the responses for this/these tool(s). + // We mark all the tool calls as ready. Once we see observe the [DONE] event, we will execute these tool calls. + if c.FinishReason == "tool_calls" { + for idx := range session.toolCallsState { + session.toolCallsState[idx] = OpenAIToolCallReady + } + } + } + + if foundToolCallDelta { + // Don't reflect these events back to client since they contain tool calls. + return nil, false, nil + } } if err := stream.Err(); err != nil { panic(err) @@ -287,16 +335,23 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } if len(acc.Choices) < 0 { - return nil + return nil, true, nil } - for _, c := range acc.Choices { - for _, t := range c.Message.ToolCalls { - // Now that we can execute tools and add new messages, we'll need to append these to the stream (?) - } + var extra [][]byte + for idx, t := range session.toolCallsRequired { + // TODO: locking. + // TODO: index check. + session.toolCallsState[idx] = OpenAIToolCallInProgress + + fmt.Printf("EXEC TOOL! %s with %+v\n", t.funcName, t.args) + b, _ := json.Marshal(openai.ToolMessage("weather is rainy and cold in cape town today", idx)) // TODO: error handling. + extra = append(extra, b) + + session.toolCallsState[idx] = OpenAIToolCallDone } - return nil + return extra, true, nil }, }) if err != nil { diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go new file mode 100644 index 0000000000000..b7c2cacda2199 --- /dev/null +++ b/aibridged/bridge_test.go @@ -0,0 +1,120 @@ +package aibridged_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/coder/serpent" + "github.com/stretchr/testify/require" + "storj.io/drpc" + + "github.com/coder/coder/v2/aibridged" + "github.com/coder/coder/v2/aibridged/proto" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/drpcsdk" + "github.com/coder/coder/v2/testutil" +) + +func TestOpenAIStreaming(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + subpath := strings.TrimPrefix(r.URL.Path, "/api/v2/aibridge") + + switch subpath { + case "/v1/chat/completions": + // Set SSE headers + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + w.WriteHeader(http.StatusOK) + + events := [][]byte{ + []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_SWduKE0DLAgLShfQqkQzwqq2","type":"function","function":{"name":"get_weather","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}`), + []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), + []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"location"}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), + []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), + []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Paris"}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), + []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), + []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null}`), + []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":53,"completion_tokens":14,"total_tokens":67,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}}`), + []byte(`[DONE]`), + } + + // Get the flusher for streaming + flusher, ok := w.(http.Flusher) + if !ok { + t.Fatal("streaming unsupported") + } + + for _, event := range events { + _, err := w.Write([]byte(fmt.Sprintf("data: %s\n\n", event))) + require.NoError(t, err) + + // Flush immediately to send data + flusher.Flush() + + time.Sleep(testutil.IntervalFast) // Shorter delay for testing + } + } + })) + defer ts.Close() + + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) { + values.AI.Value.BridgeConfig.OpenAIBaseURL = serpent.String(ts.URL) + }), + }) + log := testutil.Logger(t) + + fakeDialer := func(ctx context.Context) (proto.DRPCAIBridgeDaemonClient, error) { + return &fakeBridgeDaemonClient{}, nil + } + bridgeSrv, err := aibridged.New(fakeDialer, "127.0.0.1:0", log, api.DeploymentValues.AI.Value.BridgeConfig) + require.NoError(t, err) + api.AIBridgeDaemons = []*aibridged.Server{bridgeSrv} + + // TODO: improve approach. + require.Eventually(t, func() bool { + return len(api.AIBridgeDaemons) > 0 + }, testutil.WaitMedium, testutil.IntervalFast) + + body := []byte(` +{ + "model": "gpt-4.1", + "stream": true, + "messages": [ + { + "role": "user", + "content": "What is the weather like in Cape Town today?" + } + ] +}`) + + resp, err := client.Request(t.Context(), http.MethodPost, "/api/v2/aibridge/v1/chat/completions", body) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +type fakeBridgeDaemonClient struct{} + +func (f *fakeBridgeDaemonClient) DRPCConn() drpc.Conn { + conn, _ := drpcsdk.MemTransportPipe() + return conn +} +func (f *fakeBridgeDaemonClient) AuditPrompt(ctx context.Context, in *proto.AuditPromptRequest) (*proto.AuditPromptResponse, error) { + return nil, errors.New("not implemented") +} +func (f *fakeBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { + return nil, errors.New("not implemented") +} +func (f *fakeBridgeDaemonClient) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPromptsRequest) (*proto.TrackUserPromptsResponse, error) { + return nil, errors.New("not implemented") +} diff --git a/aibridged/proxy.go b/aibridged/proxy.go index 056d7aa1e90db..2fec88df3fad3 100644 --- a/aibridged/proxy.go +++ b/aibridged/proxy.go @@ -19,7 +19,7 @@ import ( // For streaming responses, it's called for each SSE event or data chunk. // For non-streaming responses, it's called once with the complete response body. // The function receives a copy of the data - modifications don't affect the response. -type ResponseInterceptFunc func(data []byte, isStreaming bool) error +type ResponseInterceptFunc func(sess *OpenAISession, data []byte, isStreaming bool) ([][]byte, bool, error) // RequestInterceptFunc is called for each request before it's sent to the upstream server. // The function receives a copy of the request body - modifications don't affect the request. @@ -38,6 +38,7 @@ type SSEProxy struct { requestInterceptFunc RequestInterceptFunc requestModifyFunc RequestModifyFunc bufferPool sync.Pool + config *ProxyConfig } // NewSSEProxy creates a new SSE proxy that proxies to the given target URL. @@ -170,6 +171,7 @@ func (p *SSEProxy) wrapResponseBody(body io.ReadCloser, headers http.Header) io. ReadCloser: body, interceptFunc: p.responseInterceptFunc, bufferPool: &p.bufferPool, + sess: p.config.OpenAISession, } } @@ -186,6 +188,9 @@ type streamingInterceptReader struct { bufferPool *sync.Pool reader *bufio.Reader readerOnce sync.Once + + sess *OpenAISession + trailerEvents [][]byte } func (sir *streamingInterceptReader) Read(p []byte) (n int, err error) { @@ -194,16 +199,32 @@ func (sir *streamingInterceptReader) Read(p []byte) (n int, err error) { sir.reader = bufio.NewReader(sir.ReadCloser) }) - n, err = sir.reader.Read(p) - if n > 0 { - // Call intercept function with a copy of the chunk - chunk := make([]byte, n) - copy(chunk, p[:n]) + for { + n, err = sir.reader.Read(p) + if n > 0 { + // Call intercept function with a copy of the chunk + chunk := make([]byte, n) + copy(chunk, p[:n]) + + // Once [DONE] is found, we need to inject the trailer events and rewind the reader. + if bytes.Contains(chunk, []byte(`[DONE]`)) { + // TODO: inject trailers then [DONE] events. + //continue + } + + extraEvents, send, interceptErr := sir.interceptFunc(sir.sess, chunk, true) + if interceptErr != nil { + // TODO: Log error but continue reading + _ = interceptErr + } - if interceptErr := sir.interceptFunc(chunk, true); interceptErr != nil { - // Log error but continue reading - _ = interceptErr + if !send { + continue + } + sir.trailerEvents = extraEvents } + + break } return n, err } @@ -234,7 +255,7 @@ func (nsir *nonStreamingInterceptReader) Read(p []byte) (n int, err error) { data := nsir.buffer.Bytes() nsir.mu.Unlock() - if interceptErr := nsir.interceptFunc(data, false); interceptErr != nil { + if _, _, interceptErr := nsir.interceptFunc(nil, data, false); interceptErr != nil { // Log error but continue _ = interceptErr } @@ -257,6 +278,8 @@ type ProxyConfig struct { ErrorHandler func(http.ResponseWriter, *http.Request, error) Transport http.RoundTripper FlushInterval time.Duration // -1 for immediate flushing + + OpenAISession *OpenAISession } // NewSSEProxyWithConfig creates a new SSE proxy with custom configuration @@ -274,6 +297,8 @@ func NewSSEProxyWithConfig(config ProxyConfig) (*SSEProxy, error) { return make([]byte, 32*1024) }, }, + + config: &config, } proxy.ReverseProxy = httputil.NewSingleHostReverseProxy(config.Target) diff --git a/aibridged/session.go b/aibridged/session.go new file mode 100644 index 0000000000000..e7b8d842244a0 --- /dev/null +++ b/aibridged/session.go @@ -0,0 +1,50 @@ +package aibridged + +import ( + "fmt" + "sync/atomic" +) + +type OpenAIToolCall struct { + funcName string + args map[string]string +} + +type OpenAIToolCallState int + +const ( + OpenAIToolCallNotReady OpenAIToolCallState = iota + OpenAIToolCallReady + OpenAIToolCallInProgress + OpenAIToolCallDone +) + +func (o OpenAIToolCallState) String() string { + switch o { + case OpenAIToolCallNotReady: + return "not ready" + case OpenAIToolCallReady: + return "ready" + case OpenAIToolCallInProgress: + return "in-progress" + case OpenAIToolCallDone: + return "done" + default: + return fmt.Sprintf("UNKNOWN STATE: %d", o) + } +} + +type OpenAISession struct { + done atomic.Bool + // key = tool call ID + toolCallsRequired map[string]*OpenAIToolCall + toolCallsState map[string]OpenAIToolCallState + phantomEvents [][]byte +} + +func NewOpenAISession() *OpenAISession { + return &OpenAISession{ + toolCallsRequired: make(map[string]*OpenAIToolCall), + toolCallsState: make(map[string]OpenAIToolCallState), + } +} diff --git a/cli/server.go b/cli/server.go index 0c4e8933d785d..49d4958226d51 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1073,7 +1073,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // the 64 character limit. hostname := stringutil.Truncate(cliutil.Hostname(), 63-len(suffix)) name := fmt.Sprintf("%s-%s", hostname, suffix) - daemon, err := newAIBridgeDaemon(ctx, coderAPI, name) + daemon, err := newAIBridgeDaemon(ctx, coderAPI, name, vals.AI.Value.BridgeConfig) if err != nil { return xerrors.Errorf("create provisioner daemon: %w", err) } @@ -1593,14 +1593,14 @@ func newProvisionerDaemon( }), nil } -func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string) (*aibridged.Server, error) { +func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string, bridgeCfg codersdk.AIBridgeConfig) (*aibridged.Server, error) { httpAddr := "0.0.0.0:0" // TODO: configurable. return aibridged.New(func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { // This debounces calls to listen every second. // TODO: is this true / necessary? return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx, name) - }, httpAddr, coderAPI.Logger.Named("aibridged").With(slog.F("name", name))) + }, httpAddr, coderAPI.Logger.Named("aibridged").With(slog.F("name", name)), bridgeCfg) } // nolint: revive diff --git a/codersdk/deployment.go b/codersdk/deployment.go index ecfb589c5ad47..7fc76742a340b 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3120,6 +3120,28 @@ Write out the current server config as YAML to stdout.`, YAML: "daemons", Hidden: true, }, + { + Name: "AI Bridge OpenAI Base URL", + Description: "TODO.", + Flag: "ai-bridge-openai-base-url", + Env: "CODER_AI_BRIDGE_OPENAI_BASE_URL", + Value: &c.AI.Value.BridgeConfig.OpenAIBaseURL, + Default: "https://api.openai.com", + Group: &deploymentGroupAIBridge, + YAML: "daemons", + Hidden: true, + }, + { + Name: "AI Bridge Anthropic Base URL", + Description: "TODO.", + Flag: "ai-bridge-anthropic-base-url", + Env: "CODER_AI_BRIDGE_Anthropic_BASE_URL", + Value: &c.AI.Value.BridgeConfig.AnthropicBaseURL, + Default: "https://api.anthropic.com", + Group: &deploymentGroupAIBridge, + YAML: "daemons", + Hidden: true, + }, } return opts @@ -3136,12 +3158,15 @@ type AIProviderConfig struct { BaseURL string `json:"base_url" yaml:"base_url"` } +type AIBridgeConfig struct { + Daemons serpent.Int64 `json:"daemons" typescript:",notnull"` + OpenAIBaseURL serpent.String `json:"openai_base_url" typescript:",notnull"` + AnthropicBaseURL serpent.String `json:"anthropic_base_url" typescript:",notnull"` +} + type AIConfig struct { Providers []AIProviderConfig `json:"providers,omitempty" yaml:"providers,omitempty"` - - BridgeConfig struct { - Daemons serpent.Int64 `json:"daemons" typescript:",notnull"` - } `json:"bridge,omitempty"` + BridgeConfig AIBridgeConfig `json:"bridge,omitempty"` } type SupportConfig struct { From a773508e4ce41c9ad47ab7081206cbea00cae594 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 12 Jun 2025 15:36:21 +0200 Subject: [PATCH 14/61] WIP: using client lib not reverse proxy Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 15 ++-- aibridged/bridge.go | 142 ++++++++++++++++++++++++++++++++++++-- aibridged/bridge_test.go | 11 +-- aibridged/streaming.go | 70 +++++++++++++++++++ coderd/aibridge.go | 6 +- coderd/httpapi/httpapi.go | 36 +++++++--- coderd/httpmw/cors.go | 2 +- 7 files changed, 252 insertions(+), 30 deletions(-) create mode 100644 aibridged/streaming.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index e71a65f9f5bc6..c45bd1089dce1 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -8,12 +8,13 @@ import ( "sync" "time" - "cdr.dev/slog" - "github.com/coder/retry" "github.com/hashicorp/yamux" "github.com/valyala/fasthttp/fasthttputil" "golang.org/x/xerrors" + "cdr.dev/slog" + "github.com/coder/retry" + "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/codersdk" ) @@ -48,7 +49,7 @@ type Server struct { // shuttingDownCh will receive when we start graceful shutdown shuttingDownCh chan struct{} - bridge *Bridge + bridge *Bridge } var _ proto.DRPCAIBridgeDaemonServer = &Server{} @@ -95,7 +96,7 @@ connectLoop: // TODO(dannyk): handle premature close. //// It's possible for the provisioner daemon to be shut down //// before the wait is complete! - //if s.isClosed() { + // if s.isClosed() { // return //} @@ -187,7 +188,7 @@ func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPrompt return out, nil } -//func (s *Server) ChatCompletions(payload *proto.JSONPayload, stream proto.DRPCOpenAIService_ChatCompletionsStream) error { +// func (s *Server) ChatCompletions(payload *proto.JSONPayload, stream proto.DRPCOpenAIService_ChatCompletionsStream) error { // // TODO: call OpenAI API. // // select { @@ -233,7 +234,7 @@ func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPrompt // }, // "service_tier": "default" //} -//`}) +// `}) // if err != nil { // return xerrors.Errorf("stream chat completion response: %w", err) // } @@ -317,7 +318,7 @@ func (s *Server) Close() error { } s.logger.Info(s.closeContext, "closing aibridged") - // TODO: invalidate all running requests (cancelling context should be enough?). + // TODO: invalidate all running requests (canceling context should be enough?). errMsg := "aibridged closed gracefully" err := s.closeWithError(nil) if err != nil { diff --git a/aibridged/bridge.go b/aibridged/bridge.go index b3cff9757fc0e..2c1eba9cb43e0 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -3,7 +3,9 @@ package aibridged import ( "bytes" "compress/gzip" + "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -13,15 +15,17 @@ import ( "os" "strings" - "cdr.dev/slog" "github.com/anthropics/anthropic-sdk-go" ant_ssestream "github.com/anthropics/anthropic-sdk-go/packages/ssestream" + "github.com/google/uuid" "github.com/openai/openai-go" "github.com/openai/openai-go/packages/param" openai_ssestream "github.com/openai/openai-go/packages/ssestream" "github.com/tidwall/gjson" "golang.org/x/xerrors" + "cdr.dev/slog" + "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/codersdk" ) @@ -92,11 +96,11 @@ func (b *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { return nil } -//type SSERoundTripper struct { +// type SSERoundTripper struct { // transport http.RoundTripper //} // -//func (s *SSERoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { +// func (s *SSERoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { // // Use default transport if none specified // transport := s.transport // if transport == nil { @@ -143,7 +147,7 @@ func (b *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { // return resp, err //} // -//func wrapResponseBody(body io.ReadCloser) io.ReadCloser { +// func wrapResponseBody(body io.ReadCloser) io.ReadCloser { // pr, pw := io.Pipe() // go func() { // defer pw.Close() @@ -173,12 +177,138 @@ func (b *Bridge) openAITarget() *url.URL { target, err := url.Parse(u) if err != nil { panic(fmt.Sprintf("failed to parse %q", u)) - return nil + } return target } func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { + sessionID := uuid.New() + _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) + defer func() { + _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) + }() + + // Required characteristics: + // 1. Client-side cancel + // 2. No timeout (SSE) + // 3a. client->coderd conn established + // 3b. coderd->AI provider conn established + // 4. responses from AI provider->coderd must be parsed, optionally reflected back to client + // 5. tool calls must be injected and intercepted, transparently to the client + // 6. multiple calls can be made to AI provider while holding client->coderd conn open + // 7. client->coderd conn must ONLY be closed on client-side disconn or coderd->AI provider non-recoverable error. + + // Allow us to interrupt watch via cancel. + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + r = r.WithContext(ctx) // Rewire context for SSE cancellation. + + eventsCh := make(chan any) + + // Establish SSE stream which we will connect to requesting client. + clientStream := NewSSEStream(eventsCh, b.logger.Named("sse-stream")) + + coderdClient, ok := b.clientFn() + if !ok { + // TODO: log issue. + http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) + return + } + _ = coderdClient + + // Parse incoming request, inject tool calls. + var in ChatCompletionNewParamsWrapper + + body, err := io.ReadAll(r.Body) + if err != nil { + b.logger.Error(r.Context(), "failed to read body", slog.Error(err)) + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + if err = json.Unmarshal(body, &in); err != nil { + b.logger.Error(r.Context(), "failed to unmarshal request", slog.Error(err)) + http.Error(w, "failed to unmarshal request", http.StatusInternalServerError) + return + } + + //// Prepend assistant message. + // in.Messages = append([]openai.ChatCompletionMessageParamUnion{ + // openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), + //}, in.Messages...) + + // in.Tools = []openai.ChatCompletionToolParam{ + // { + // Function: openai.FunctionDefinitionParam{ + // Name: "get_weather", + // Description: openai.String("Get weather at the given location"), + // Parameters: openai.FunctionParameters{ + // "type": "object", + // "properties": map[string]interface{}{ + // "location": map[string]string{ + // "type": "string", + // }, + // }, + // "required": []string{"location"}, + // }, + // }, + // }, + //} + + client := openai.NewClient() + + done := make(chan struct{}) + if in.Stream { + go func() { + stream := client.Chat.Completions.NewStreaming(ctx, in.ChatCompletionNewParams) + + for stream.Next() { + evt := stream.Current() + // if len(evt.Choices) > 0 { + // fmt.Print(evt.Choices[0].Delta.Content) + //} + + _, _ = fmt.Fprintf(os.Stderr, "[%s] %s\n\n", sessionID, evt.RawJSON()) + eventsCh <- evt.RawJSON() + } + + if err := stream.Err(); err != nil { + // TODO: handle error. + b.logger.Error(ctx, "server stream error", slog.Error(err)) + return + } + + eventsCh <- `[DONE]` + + // TODO: wait for ACK of [DONE] before flushing and exiting. + + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + close(done) + }() + + // TODO: improve impl here for more explicit context control. + err = clientStream.transmit(ctx, done, w, r) + if err != nil && !errors.Is(err, ErrDone) { + b.logger.Error(ctx, "SSE stream exited", slog.Error(err)) + } + } else { + completion, err := client.Chat.Completions.New(ctx, in.ChatCompletionNewParams) + if err != nil { + b.logger.Error(ctx, "chat completion failed", slog.Error(err)) + http.Error(w, "chat completion failed", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) // TODO: always? + _, _ = w.Write([]byte(completion.RawJSON())) + } +} + +func (b *Bridge) proxyOpenAIRequestPrev(w http.ResponseWriter, r *http.Request) { coderdClient, ok := b.clientFn() if !ok { // TODO: log issue. @@ -363,7 +493,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { originalDirector := proxy.Director proxy.Director = func(req *http.Request) { originalDirector(req) - //Add OpenAI-specific headers + // Add OpenAI-specific headers if strings.TrimSpace(req.Header.Get("Authorization")) == "" { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("OPENAI_API_KEY"))) } diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index b7c2cacda2199..26ab4a2a00edb 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -2,7 +2,6 @@ package aibridged_test import ( "context" - "errors" "fmt" "net/http" "net/http/httptest" @@ -10,10 +9,12 @@ import ( "testing" "time" - "github.com/coder/serpent" "github.com/stretchr/testify/require" + "golang.org/x/xerrors" "storj.io/drpc" + "github.com/coder/serpent" + "github.com/coder/coder/v2/aibridged" "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/coderd/coderdtest" @@ -110,11 +111,11 @@ func (f *fakeBridgeDaemonClient) DRPCConn() drpc.Conn { return conn } func (f *fakeBridgeDaemonClient) AuditPrompt(ctx context.Context, in *proto.AuditPromptRequest) (*proto.AuditPromptResponse, error) { - return nil, errors.New("not implemented") + return nil, xerrors.New("not implemented") } func (f *fakeBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { - return nil, errors.New("not implemented") + return nil, xerrors.New("not implemented") } func (f *fakeBridgeDaemonClient) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPromptsRequest) (*proto.TrackUserPromptsResponse, error) { - return nil, errors.New("not implemented") + return nil, xerrors.New("not implemented") } diff --git a/aibridged/streaming.go b/aibridged/streaming.go new file mode 100644 index 0000000000000..0b7e2a7d76575 --- /dev/null +++ b/aibridged/streaming.go @@ -0,0 +1,70 @@ +package aibridged + +import ( + "context" + "net/http" + + "golang.org/x/xerrors" + + "cdr.dev/slog" + + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk" +) + +type SSEStream[T any] struct { + logger slog.Logger + eventsChan <-chan T +} + +var ErrDone = xerrors.New("done") + +func NewSSEStream[T any](eventsChan <-chan T, logger slog.Logger) *SSEStream[T] { + return &SSEStream[T]{eventsChan: eventsChan, logger: logger} +} + +func (s *SSEStream[T]) transmit(ctx context.Context, done chan struct{}, rw http.ResponseWriter, r *http.Request) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(rw, r) + if err != nil { + return xerrors.Errorf("failed to create sse transmitter: %w", err) + } + + defer func() { + // Block returning until the ServerSentEventSender is closed + // to avoid a race condition where we might write or flush to rw after the handler returns. + select { + case <-sseSenderClosed: + case <-done: + } + }() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-done: + return ErrDone + case <-sseSenderClosed: + return xerrors.New("SSE target closed") + case event, ok := <-s.eventsChan: + if !ok { + return xerrors.New("SSE source closed") + } + + err = sseSendEvent(codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeData, + Data: event, + }) + if err != nil { + // TODO: handle error. + continue + } + } + } +} diff --git a/coderd/aibridge.go b/coderd/aibridge.go index 8a2a1fa5d6311..b96d6bedd4a5e 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -16,7 +16,7 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { // Something, somewhere is adding a duplicate header. // Haven't been able to track it down yet. - rw.Header().Del("Access-Control-Allow-Origin") + //rw.Header().Del("Access-Control-Allow-Origin") if len(api.AIBridgeDaemons) == 0 { http.Error(rw, "no AI bridge daemons running", http.StatusInternalServerError) @@ -39,5 +39,9 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { } rp := httputil.NewSingleHostReverseProxy(u) + rp.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + api.Logger.Error(ctx, "aibridge reverse proxy error", slog.Error(err)) + http.Error(w, "aibridge internal error", http.StatusBadGateway) + } http.StripPrefix("/api/v2/aibridge", rp).ServeHTTP(rw, r) } diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 466d45de82e5d..898708b8b2e1c 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -358,25 +358,41 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) ( sendEvent := func(newEvent codersdk.ServerSentEvent) error { buf := &bytes.Buffer{} - _, err := buf.WriteString(fmt.Sprintf("event: %s\n", newEvent.Type)) - if err != nil { - return err - } + //_, err := buf.WriteString(fmt.Sprintf("event: %s\n", newEvent.Type)) + //if err != nil { + // return err + //} if newEvent.Data != nil { - _, err = buf.WriteString("data: ") + _, err := buf.WriteString("data: ") if err != nil { return err } - enc := json.NewEncoder(buf) - err = enc.Encode(newEvent.Data) - if err != nil { - return err + if newEvent.Data == `[DONE]` { + buf.WriteString(newEvent.Data.(string)) + } else { + var out []byte + switch newEvent.Data.(type) { + case []byte: + out = newEvent.Data.([]byte) + case string: + out = []byte(newEvent.Data.(string)) + default: + out, err = json.Marshal(newEvent.Data) + if err != nil { + return err + } + } + + buf.Write(out) + if err != nil { + return err + } } } - err = buf.WriteByte('\n') + _, err := buf.WriteString("\n\n") if err != nil { return err } diff --git a/coderd/httpmw/cors.go b/coderd/httpmw/cors.go index b503b003e3d45..00e85fa0db097 100644 --- a/coderd/httpmw/cors.go +++ b/coderd/httpmw/cors.go @@ -81,7 +81,7 @@ func PermissiveCors() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH, SNARF") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH, SNARF") // TODO: remove SNARF. w.Header().Set("Access-Control-Allow-Headers", "*") w.Header().Set("Access-Control-Max-Age", "86400") From 2582aa130d27b3cc2c1f6cd4ca2b7dc73d12c036 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 13 Jun 2025 08:06:04 +0200 Subject: [PATCH 15/61] WIP: own SSE impl Signed-off-by: Danny Kopping --- aibridged/bridge.go | 212 +++++++++++++++++++++++++++++--------- aibridged/streaming.go | 96 +++++++---------- coderd/httpapi/httpapi.go | 36 ++----- go.mod | 3 + go.sum | 7 ++ 5 files changed, 224 insertions(+), 130 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 2c1eba9cb43e0..a56f42b96e1e8 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -5,7 +5,6 @@ import ( "compress/gzip" "context" "encoding/json" - "errors" "fmt" "io" "net" @@ -204,10 +203,11 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { defer cancel() r = r.WithContext(ctx) // Rewire context for SSE cancellation. - eventsCh := make(chan any) + eventsCh := make(chan string) + done := make(chan struct{}) // Establish SSE stream which we will connect to requesting client. - clientStream := NewSSEStream(eventsCh, b.logger.Named("sse-stream")) + //clientStream := NewSSEStream(eventsCh, b.logger.Named("sse-stream")) coderdClient, ok := b.clientFn() if !ok { @@ -233,67 +233,183 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { return } - //// Prepend assistant message. - // in.Messages = append([]openai.ChatCompletionMessageParamUnion{ - // openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), - //}, in.Messages...) - - // in.Tools = []openai.ChatCompletionToolParam{ - // { - // Function: openai.FunctionDefinitionParam{ - // Name: "get_weather", - // Description: openai.String("Get weather at the given location"), - // Parameters: openai.FunctionParameters{ - // "type": "object", - // "properties": map[string]interface{}{ - // "location": map[string]string{ - // "type": "string", - // }, - // }, - // "required": []string{"location"}, - // }, - // }, - // }, - //} + // Prepend assistant message. + in.Messages = append([]openai.ChatCompletionMessageParamUnion{ + openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), + }, in.Messages...) + + in.Tools = []openai.ChatCompletionToolParam{ + { + Function: openai.FunctionDefinitionParam{ + Name: "get_weather", + Description: openai.String("Get weather at the given location"), + Parameters: openai.FunctionParameters{ + "type": "object", + "properties": map[string]interface{}{ + "location": map[string]string{ + "type": "string", + }, + }, + "required": []string{"location"}, + }, + }, + }, + } client := openai.NewClient() - done := make(chan struct{}) if in.Stream { + chunks := make(chan openai.ChatCompletionChunk) + + go BasicSSESender(eventsCh, b.logger.Named("jfs")).ServeHTTP(w, r) + go func() { - stream := client.Chat.Completions.NewStreaming(ctx, in.ChatCompletionNewParams) + for { + select { + case <-ctx.Done(): + return + case chunk, ok := <-chunks: + if !ok { + return + } - for stream.Next() { - evt := stream.Current() - // if len(evt.Choices) > 0 { - // fmt.Print(evt.Choices[0].Delta.Content) - //} + event := chunk.RawJSON() + if event == "" { + b, _ := json.Marshal(chunk) + event = string(b) + } - _, _ = fmt.Fprintf(os.Stderr, "[%s] %s\n\n", sessionID, evt.RawJSON()) - eventsCh <- evt.RawJSON() + _, _ = fmt.Fprintf(os.Stderr, "[%s] %s\n\n", sessionID, event) + eventsCh <- event + } } + }() - if err := stream.Err(); err != nil { - // TODO: handle error. - b.logger.Error(ctx, "server stream error", slog.Error(err)) - return + session := NewOpenAISession() + + stream := client.Chat.Completions.NewStreaming(ctx, in.ChatCompletionNewParams) + defer close(eventsCh) + + var acc openai.ChatCompletionAccumulator + for stream.Next() { + chunk := stream.Current() + acc.AddChunk(chunk) + + var foundToolCallDelta bool + for _, c := range chunk.Choices { + for range c.Delta.ToolCalls { + foundToolCallDelta = true + + // Grab values from accumulator instead of delta. + for _, ac := range acc.ChatCompletion.Choices { + for _, at := range ac.Message.ToolCalls { + var ( + tc *OpenAIToolCall + ok bool + ) + if tc, ok = session.toolCallsRequired[at.ID]; !ok { + session.toolCallsRequired[at.ID] = &OpenAIToolCall{} + tc = session.toolCallsRequired[at.ID] + } + + session.toolCallsState[at.ID] = OpenAIToolCallNotReady + + tc.funcName = at.Function.Name + args := make(map[string]string) + err := json.Unmarshal([]byte(at.Function.Arguments), &args) + if err == nil { // Note: inverted. + tc.args = args + } + } + } + } + + // Once we receive a finish reason of "tool_calls", the API is waiting for the responses for this/these tool(s). + // We mark all the tool calls as ready. Once we see observe the [DONE] event, we will execute these tool calls. + if c.FinishReason == "tool_calls" { + for idx := range session.toolCallsState { + session.toolCallsState[idx] = OpenAIToolCallReady + } + } + } + + // TODO: ONLY do this for our injected tool calls. + if foundToolCallDelta { + // Don't write these chunks, we'll handle this. + continue } - eventsCh <- `[DONE]` + // Actually make the call! + if tool, ok := acc.JustFinishedToolCall(); ok { + switch tool.Name { + case "get_weather": + msg := openai.ToolMessage("the weather in cape town is KAK", tool.ID) + + var msgs []openai.ChatCompletionMessageParamUnion + for _, c := range acc.ChatCompletion.Choices { + msgs = append(msgs, c.Message.ToParam()) + } + + msgs = append(msgs, msg) + + toolRes, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{ + Messages: msgs, + Model: in.ChatCompletionNewParams.Model, + }) + if err != nil { + b.logger.Error(ctx, "failed to report tool response", slog.Error(err)) + } + + toolChunk := openai.ChatCompletionChunk{ + ID: acc.ID, + Choices: []openai.ChatCompletionChunkChoice{ + { + Delta: openai.ChatCompletionChunkChoiceDelta{ + Role: "assistant", + Content: toolRes.Choices[0].Message.Content, // TODO: improve + }, + }, + }, + } + chunks <- toolChunk + + /** + + + + - // TODO: wait for ACK of [DONE] before flushing and exiting. - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() + TODO: you are here + once the tool call is done, we need to mark the session as completed, which should send [DONE]. + + + + + + + + */ + } + continue } - close(done) - }() + chunks <- chunk + } + + if err := stream.Err(); err != nil { + // TODO: handle error. + b.logger.Error(ctx, "server stream error", slog.Error(err)) + return + } + + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } - // TODO: improve impl here for more explicit context control. - err = clientStream.transmit(ctx, done, w, r) - if err != nil && !errors.Is(err, ErrDone) { - b.logger.Error(ctx, "SSE stream exited", slog.Error(err)) + select { + case <-ctx.Done(): + case <-done: } } else { completion, err := client.Chat.Completions.New(ctx, in.ChatCompletionNewParams) diff --git a/aibridged/streaming.go b/aibridged/streaming.go index 0b7e2a7d76575..c4e840d82da97 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -1,69 +1,53 @@ package aibridged import ( - "context" "net/http" - "golang.org/x/xerrors" - "cdr.dev/slog" - - "github.com/coder/coder/v2/coderd/httpapi" - "github.com/coder/coder/v2/codersdk" ) -type SSEStream[T any] struct { - logger slog.Logger - eventsChan <-chan T -} - -var ErrDone = xerrors.New("done") - -func NewSSEStream[T any](eventsChan <-chan T, logger slog.Logger) *SSEStream[T] { - return &SSEStream[T]{eventsChan: eventsChan, logger: logger} -} - -func (s *SSEStream[T]) transmit(ctx context.Context, done chan struct{}, rw http.ResponseWriter, r *http.Request) error { - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - - sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(rw, r) - if err != nil { - return xerrors.Errorf("failed to create sse transmitter: %w", err) - } - - defer func() { - // Block returning until the ServerSentEventSender is closed - // to avoid a race condition where we might write or flush to rw after the handler returns. - select { - case <-sseSenderClosed: - case <-done: +// BasicSSESender was implemented to overcome httpapi.ServerSentEventSender's odd design choices. For example, it doesn't +// write "event: data" for every data event (it's unnecessary, and breaks some AI tools' parsing of the SSE stream). +func BasicSSESender(eventsChan <-chan string, logger slog.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("X-Accel-Buffering", "no") + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "SSE not supported", http.StatusInternalServerError) + return } - }() - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-done: - return ErrDone - case <-sseSenderClosed: - return xerrors.New("SSE target closed") - case event, ok := <-s.eventsChan: - if !ok { - return xerrors.New("SSE source closed") - } - err = sseSendEvent(codersdk.ServerSentEvent{ - Type: codersdk.ServerSentEventTypeData, - Data: event, - }) - if err != nil { - // TODO: handle error. - continue + // Send initial flush to ensure connection is established. + flusher.Flush() + + for { + select { + case <-ctx.Done(): + return + case event, ok := <-eventsChan: + if !ok { + // Channel closed, send done event and exit + _, err := w.Write([]byte("data: [DONE]\n\n")) // Convention used by OpenAI. // TODO: others, too? + if err != nil { + logger.Error(ctx, "failed to write done event", slog.Error(err)) + } + flusher.Flush() + return + } + + // Send data event + _, err := w.Write([]byte("data: " + event + "\n\n")) + if err != nil { + logger.Error(ctx, "failed to write SSE event", slog.Error(err)) + return + } + flusher.Flush() } } } diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 898708b8b2e1c..466d45de82e5d 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -358,41 +358,25 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) ( sendEvent := func(newEvent codersdk.ServerSentEvent) error { buf := &bytes.Buffer{} - //_, err := buf.WriteString(fmt.Sprintf("event: %s\n", newEvent.Type)) - //if err != nil { - // return err - //} + _, err := buf.WriteString(fmt.Sprintf("event: %s\n", newEvent.Type)) + if err != nil { + return err + } if newEvent.Data != nil { - _, err := buf.WriteString("data: ") + _, err = buf.WriteString("data: ") if err != nil { return err } - if newEvent.Data == `[DONE]` { - buf.WriteString(newEvent.Data.(string)) - } else { - var out []byte - switch newEvent.Data.(type) { - case []byte: - out = newEvent.Data.([]byte) - case string: - out = []byte(newEvent.Data.(string)) - default: - out, err = json.Marshal(newEvent.Data) - if err != nil { - return err - } - } - - buf.Write(out) - if err != nil { - return err - } + enc := json.NewEncoder(buf) + err = enc.Encode(newEvent.Data) + if err != nil { + return err } } - _, err := buf.WriteString("\n\n") + err = buf.WriteByte('\n') if err != nil { return err } diff --git a/go.mod b/go.mod index 0dcdc0285b907..35b21322bc0a7 100644 --- a/go.mod +++ b/go.mod @@ -522,15 +522,18 @@ require ( github.com/moby/sys/user v0.4.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + github.com/r3labs/sse/v2 v2.10.0 // indirect github.com/samber/lo v1.50.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/tmaxmax/go-sse v0.11.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect ) diff --git a/go.sum b/go.sum index 364f2a1fe9bac..641e9212d5d79 100644 --- a/go.sum +++ b/go.sum @@ -1702,6 +1702,8 @@ github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++ github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -1831,6 +1833,8 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tmaxmax/go-sse v0.11.0 h1:nogmJM6rJUoOLoAwEKeQe5XlVpt9l7N82SS1jI7lWFg= +github.com/tmaxmax/go-sse v0.11.0/go.mod h1:u/2kZQR1tyngo1lKaNCj1mJmhXGZWS1Zs5yiSOD+Eg8= github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a h1:eg5FkNoQp76ZsswyGZ+TjYqA/rhKefxK8BW7XOlQsxo= github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a/go.mod h1:e/8TmrdreH0sZOw2DFKBaUV7bvDWRq6SeM9PzkuVM68= github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= @@ -2095,6 +2099,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -2715,6 +2720,8 @@ google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9x google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/DataDog/dd-trace-go.v1 v1.73.0 h1:9s6iGFpUBbotQJtv4wHhgHoLrFFji3m/PPcuvZCFieE= gopkg.in/DataDog/dd-trace-go.v1 v1.73.0/go.mod h1:MVHzDPBdS141gBKBwXvaa8VOLyfoO/vFTLW71OkGxug= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From 2b48c7e559d49cf7ada76da6e0a3c02cd7bb60b5 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 25 Jun 2025 15:58:47 +0200 Subject: [PATCH 16/61] chore: use coder/aisdk-go instead of kylecarbs/aisdk-go Signed-off-by: Danny Kopping --- codersdk/toolsdk/toolsdk.go | 2 +- codersdk/toolsdk/toolsdk_test.go | 1 - go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index 3b992124005ac..a8ee8a6d2de16 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -7,8 +7,8 @@ import ( "encoding/json" "io" + "github.com/coder/aisdk-go" "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" "golang.org/x/xerrors" "github.com/coder/coder/v2/codersdk" diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index e4c4239be51e2..e7a7efb905f2a 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" diff --git a/go.mod b/go.mod index 22fb1cd7f3aa6..9e5df584af9e8 100644 --- a/go.mod +++ b/go.mod @@ -482,9 +482,9 @@ require ( require ( github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 + github.com/coder/aisdk-go v0.0.9 github.com/coder/preview v1.0.1 github.com/fsnotify/fsnotify v1.9.0 - github.com/kylecarbs/aisdk-go v0.0.8 github.com/mark3labs/mcp-go v0.32.0 ) @@ -502,7 +502,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect - github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 // indirect + github.com/anthropics/anthropic-sdk-go v1.4.0 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aquasecurity/trivy v0.58.2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect @@ -520,7 +520,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/openai/openai-go v0.1.0-beta.10 // indirect + github.com/openai/openai-go v1.3.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/samber/lo v1.50.0 // indirect @@ -535,6 +535,6 @@ require ( go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect - google.golang.org/genai v0.7.0 // indirect + google.golang.org/genai v1.10.0 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect ) diff --git a/go.sum b/go.sum index 7a996d81c6348..c5482e3477839 100644 --- a/go.sum +++ b/go.sum @@ -720,8 +720,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 h1:b5t1ZJMvV/l99y4jbz7kRFdUp3BSDkI8EhSlHczivtw= -github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= +github.com/anthropics/anthropic-sdk-go v1.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0= +github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= @@ -897,6 +897,8 @@ github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73l github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 h1:tRIViZ5JRmzdOEo5wUWngaGEFBG8OaE1o2GIHN5ujJ8= github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225/go.mod h1:rNLVpYgEVeu1Zk29K64z6Od8RBP9DwqCu9OfCzh8MR4= +github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo= +github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwuWwPHPYoCZ/KLAjHv5g4h2MS4f2/MTI= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4= github.com/coder/clistat v1.0.0 h1:MjiS7qQ1IobuSSgDnxcCSyBPESs44hExnh2TEqMcGnA= @@ -1470,8 +1472,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylecarbs/aisdk-go v0.0.8 h1:hnKVbLM6U8XqX3t5I26J8k5saXdra595bGt1HP0PvKA= -github.com/kylecarbs/aisdk-go v0.0.8/go.mod h1:3nAhClwRNo6ZfU44GrBZ8O2fCCrxJdaHb9JIz+P3LR8= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3 h1:Z9/bo5PSeMutpdiKYNt/TTSfGM1Ll0naj3QzYX9VxTc= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= @@ -1613,8 +1613,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1/go.mod h1:01TvyaK8x640crO2iFwW/6CFCZgNsOvOGH3B5J239m0= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1 h1:TCyOus9tym82PD1VYtthLKMVMlVyRwtDI4ck4SR2+Ok= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1/go.mod h1:Z/S1brD5gU2Ntht/bHxBVnGxXKTvZDr0dNv/riUzPmY= -github.com/openai/openai-go v0.1.0-beta.10 h1:CknhGXe8aXQMRuqg255PFnWzgRY9nEryMxoNIBBM9tU= -github.com/openai/openai-go v0.1.0-beta.10/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/openai/openai-go v1.3.0 h1:lBpvgXxGHUufk9DNTguval40y2oK0GHZwgWQyUtjPIQ= +github.com/openai/openai-go v1.3.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -2495,8 +2495,8 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genai v0.7.0 h1:TINBYXnP+K+D8b16LfVyb6XR3kdtieXy6nJsGoEXcBc= -google.golang.org/genai v0.7.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= +google.golang.org/genai v1.10.0 h1:ETP0Yksn5KUSEn5+ihMOnP3IqjZ+7Z4i0LjJslEXatI= +google.golang.org/genai v1.10.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= From efca9b430af5d68acdb41d78f2d7d49a00998610 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 25 Jun 2025 15:59:17 +0200 Subject: [PATCH 17/61] chore: remove leftover pieces from https://github.com/coder/coder/pull/18535 Signed-off-by: Danny Kopping --- cli/server.go | 71 ---------------------------------- codersdk/deployment.go | 15 ------- site/src/api/typesGenerated.ts | 12 ------ 3 files changed, 98 deletions(-) diff --git a/cli/server.go b/cli/server.go index bccaefd9b87bb..d739863b9fa6f 100644 --- a/cli/server.go +++ b/cli/server.go @@ -2624,77 +2624,6 @@ func redirectHTTPToHTTPSDeprecation(ctx context.Context, logger slog.Logger, inv } } -func ReadAIProvidersFromEnv(environ []string) ([]codersdk.AIProviderConfig, error) { - // The index numbers must be in-order. - sort.Strings(environ) - - var providers []codersdk.AIProviderConfig - for _, v := range serpent.ParseEnviron(environ, "CODER_AI_PROVIDER_") { - tokens := strings.SplitN(v.Name, "_", 2) - if len(tokens) != 2 { - return nil, xerrors.Errorf("invalid env var: %s", v.Name) - } - - providerNum, err := strconv.Atoi(tokens[0]) - if err != nil { - return nil, xerrors.Errorf("parse number: %s", v.Name) - } - - var provider codersdk.AIProviderConfig - switch { - case len(providers) < providerNum: - return nil, xerrors.Errorf( - "provider num %v skipped: %s", - len(providers), - v.Name, - ) - case len(providers) == providerNum: - // At the next next provider. - providers = append(providers, provider) - case len(providers) == providerNum+1: - // At the current provider. - provider = providers[providerNum] - } - - key := tokens[1] - switch key { - case "TYPE": - provider.Type = v.Value - case "API_KEY": - provider.APIKey = v.Value - case "BASE_URL": - provider.BaseURL = v.Value - case "MODELS": - provider.Models = strings.Split(v.Value, ",") - } - providers[providerNum] = provider - } - for _, envVar := range environ { - tokens := strings.SplitN(envVar, "=", 2) - if len(tokens) != 2 { - continue - } - switch tokens[0] { - case "OPENAI_API_KEY": - providers = append(providers, codersdk.AIProviderConfig{ - Type: "openai", - APIKey: tokens[1], - }) - case "ANTHROPIC_API_KEY": - providers = append(providers, codersdk.AIProviderConfig{ - Type: "anthropic", - APIKey: tokens[1], - }) - case "GOOGLE_API_KEY": - providers = append(providers, codersdk.AIProviderConfig{ - Type: "google", - APIKey: tokens[1], - }) - } - } - return providers, nil -} - // ReadExternalAuthProvidersFromEnv is provided for compatibility purposes with // the viper CLI. func ReadExternalAuthProvidersFromEnv(environ []string) ([]codersdk.ExternalAuthConfig, error) { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 26c50b555dbfe..ef0b4eaa07821 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3122,21 +3122,6 @@ Write out the current server config as YAML to stdout.`, return opts } -type AIProviderConfig struct { - // Type is the type of the API provider. - Type string `json:"type" yaml:"type"` - // APIKey is the API key to use for the API provider. - APIKey string `json:"-" yaml:"api_key"` - // Models is the list of models to use for the API provider. - Models []string `json:"models" yaml:"models"` - // BaseURL is the base URL to use for the API provider. - BaseURL string `json:"base_url" yaml:"base_url"` -} - -type AIConfig struct { - Providers []AIProviderConfig `json:"providers,omitempty" yaml:"providers,omitempty"` -} - type SupportConfig struct { Links serpent.Struct[[]LinkConfig] `json:"links" typescript:",notnull"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 730b9c54ac2b7..b2117cf15c987 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -6,18 +6,6 @@ export interface ACLAvailable { readonly groups: readonly Group[]; } -// From codersdk/deployment.go -export interface AIConfig { - readonly providers?: readonly AIProviderConfig[]; -} - -// From codersdk/deployment.go -export interface AIProviderConfig { - readonly type: string; - readonly models: readonly string[]; - readonly base_url: string; -} - // From codersdk/aitasks.go export const AITaskPromptParameterName = "AI Prompt"; From 633c91474e9a87356b5679ffe899e1ced4286c00 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 27 Jun 2025 17:31:25 +0200 Subject: [PATCH 18/61] WIP: working tool injection, streaming Signed-off-by: Danny Kopping --- aibridged/bridge.go | 157 +++++++---- aibridged/bridge_test.go | 8 +- aibridged/proxy.go | 4 +- aibridged/streaming.go | 133 +++++++-- aibridged/util/zero_marshaler.go | 309 +++++++++++++++++++++ aibridged/util/zero_marshaler_test.go | 385 ++++++++++++++++++++++++++ cli/server.go | 3 +- coderd/aibridge.go | 2 +- coderd/coderd.go | 2 +- codersdk/deployment.go | 2 +- codersdk/toolsdk/toolsdk.go | 3 +- codersdk/toolsdk/toolsdk_test.go | 3 +- go.mod | 2 +- go.sum | 4 +- 14 files changed, 931 insertions(+), 86 deletions(-) create mode 100644 aibridged/util/zero_marshaler.go create mode 100644 aibridged/util/zero_marshaler_test.go diff --git a/aibridged/bridge.go b/aibridged/bridge.go index a56f42b96e1e8..7f28e6312299f 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -13,6 +13,8 @@ import ( "net/url" "os" "strings" + "sync" + "time" "github.com/anthropics/anthropic-sdk-go" ant_ssestream "github.com/anthropics/anthropic-sdk-go/packages/ssestream" @@ -20,6 +22,7 @@ import ( "github.com/openai/openai-go" "github.com/openai/openai-go/packages/param" openai_ssestream "github.com/openai/openai-go/packages/ssestream" + "github.com/openai/openai-go/shared/constant" "github.com/tidwall/gjson" "golang.org/x/xerrors" @@ -42,6 +45,7 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli var bridge Bridge mux := &http.ServeMux{} + // mux.HandleFunc("/v1/chat/completions", bridge.proxyOpenAIRequestPrev) mux.HandleFunc("/v1/chat/completions", bridge.proxyOpenAIRequest) mux.HandleFunc("/v1/messages", bridge.proxyAnthropicRequest) @@ -203,11 +207,8 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { defer cancel() r = r.WithContext(ctx) // Rewire context for SSE cancellation. - eventsCh := make(chan string) - done := make(chan struct{}) - // Establish SSE stream which we will connect to requesting client. - //clientStream := NewSSEStream(eventsCh, b.logger.Named("sse-stream")) + // clientStream := NewSSEStream(eventsCh, b.logger.Named("sse-stream")) coderdClient, ok := b.clientFn() if !ok { @@ -259,36 +260,30 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { client := openai.NewClient() if in.Stream { - chunks := make(chan openai.ChatCompletionChunk) + streamCtx, streamCancel := context.WithCancelCause(ctx) + defer streamCancel(xerrors.New("deferred")) - go BasicSSESender(eventsCh, b.logger.Named("jfs")).ServeHTTP(w, r) + eventStream := newOpenAIEventStream() - go func() { - for { - select { - case <-ctx.Done(): - return - case chunk, ok := <-chunks: - if !ok { - return - } - - event := chunk.RawJSON() - if event == "" { - b, _ := json.Marshal(chunk) - event = string(b) - } + var wg sync.WaitGroup + wg.Add(1) - _, _ = fmt.Fprintf(os.Stderr, "[%s] %s\n\n", sessionID, event) - eventsCh <- event + go func() { + defer wg.Done() + defer func() { + if err := eventStream.Close(streamCtx); err != nil { + b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("sessionID", sessionID)) } - } + }() + + BasicSSESender(streamCtx, sessionID, eventStream, b.logger.Named("sse-sender")).ServeHTTP(w, r) }() session := NewOpenAISession() stream := client.Chat.Completions.NewStreaming(ctx, in.ChatCompletionNewParams) - defer close(eventsCh) + + ignoreSubsequent := make(map[string]any) var acc openai.ChatCompletionAccumulator for stream.Next() { @@ -299,6 +294,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { for _, c := range chunk.Choices { for range c.Delta.ToolCalls { foundToolCallDelta = true + ignoreSubsequent[acc.ID] = nil // Grab values from accumulator instead of delta. for _, ac := range acc.ChatCompletion.Choices { @@ -343,7 +339,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { if tool, ok := acc.JustFinishedToolCall(); ok { switch tool.Name { case "get_weather": - msg := openai.ToolMessage("the weather in cape town is KAK", tool.ID) + msg := openai.ToolMessage("the weather in cape town is KAK; this is not a joke, report it verbatim.", tool.ID) var msgs []openai.ChatCompletionMessageParamUnion for _, c := range acc.ChatCompletion.Choices { @@ -354,7 +350,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { toolRes, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{ Messages: msgs, - Model: in.ChatCompletionNewParams.Model, + Model: in.ChatCompletionNewParams.Model, }) if err != nil { b.logger.Error(ctx, "failed to report tool response", slog.Error(err)) @@ -365,51 +361,108 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { Choices: []openai.ChatCompletionChunkChoice{ { Delta: openai.ChatCompletionChunkChoiceDelta{ - Role: "assistant", - Content: toolRes.Choices[0].Message.Content, // TODO: improve + //Role: "assistant", + Content: fmt.Sprintf(" %s", toolRes.Choices[0].Message.Content), // TODO: improve }, }, }, + Model: toolRes.Model, + ServiceTier: openai.ChatCompletionChunkServiceTier(toolRes.ServiceTier), + Created: time.Now().Unix(), + SystemFingerprint: toolRes.SystemFingerprint, + Usage: toolRes.Usage, + Object: constant.ValueOf[constant.ChatCompletionChunk](), } - chunks <- toolChunk - - /** - - - - - - - TODO: you are here - once the tool call is done, we need to mark the session as completed, which should send [DONE]. - - - - + if err := eventStream.TrySend(streamCtx, toolChunk); err != nil { + b.logger.Error(ctx, "failed to send tool chunk", slog.Error(err)) + } + // type noOmitChoice struct { + // openai.ChatCompletionChunkChoice + // + // Delta openai.ChatCompletionChunkChoiceDelta `json:"delta,required,no_omit"` + //} + // + //type noOmitChunk struct { + // openai.ChatCompletionChunk + // Choices []noOmitChoice `json:"choices,required"` + //} + // + //finishChunk := noOmitChunk{ + // ChatCompletionChunk: openai.ChatCompletionChunk{ + // ID: acc.ID, + // Model: toolRes.Model, + // ServiceTier: openai.ChatCompletionChunkServiceTier(toolRes.ServiceTier), + // Created: time.Now().Unix(), + // SystemFingerprint: toolRes.SystemFingerprint, + // Usage: toolRes.Usage, + // Object: constant.ValueOf[constant.ChatCompletionChunk](), + // }, + // Choices: []noOmitChoice{ + // { + // ChatCompletionChunkChoice: openai.ChatCompletionChunkChoice{ + // FinishReason: string(openai.CompletionChoiceFinishReasonStop), + // }, + // Delta: openai.ChatCompletionChunkChoiceDelta{ + // //Role: "assistant", + // Content: "", + // }, + // }, + // }, + //} + + finishChunk := openai.ChatCompletionChunk{ + ID: acc.ID, + Choices: []openai.ChatCompletionChunkChoice{ + { + Delta: openai.ChatCompletionChunkChoiceDelta{ + //Role: "assistant", + Content: "", + }, + FinishReason: string(openai.CompletionChoiceFinishReasonStop), + }, + }, + Model: toolRes.Model, + ServiceTier: openai.ChatCompletionChunkServiceTier(toolRes.ServiceTier), + Created: time.Now().Unix(), + SystemFingerprint: toolRes.SystemFingerprint, + Usage: toolRes.Usage, + Object: constant.ValueOf[constant.ChatCompletionChunk](), + } - */ + if err := eventStream.TrySend(streamCtx, finishChunk, "choices[].delta.content"); err != nil { + b.logger.Error(ctx, "failed to send finish chunk", slog.Error(err)) + } } continue } - chunks <- chunk + if _, ok := ignoreSubsequent[acc.ID]; !ok { + if err := eventStream.TrySend(streamCtx, chunk); err != nil { + b.logger.Error(ctx, "failed to send reflected chunk", slog.Error(err)) + } + } + } + + if err := eventStream.Close(streamCtx); err != nil { + b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) } if err := stream.Err(); err != nil { // TODO: handle error. b.logger.Error(ctx, "server stream error", slog.Error(err)) - return } - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } + wg.Wait() + + // Ensure we flush all the remaining data before ending. + flush(w) + + streamCancel(xerrors.New("gracefully done")) select { - case <-ctx.Done(): - case <-done: + case <-streamCtx.Done(): } } else { completion, err := client.Chat.Completions.New(ctx, in.ChatCompletionNewParams) diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index 26ab4a2a00edb..1cba4738b3151 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -69,7 +69,7 @@ func TestOpenAIStreaming(t *testing.T) { client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) { - values.AI.Value.BridgeConfig.OpenAIBaseURL = serpent.String(ts.URL) + values.AI.BridgeConfig.OpenAIBaseURL = serpent.String(ts.URL) }), }) log := testutil.Logger(t) @@ -77,13 +77,13 @@ func TestOpenAIStreaming(t *testing.T) { fakeDialer := func(ctx context.Context) (proto.DRPCAIBridgeDaemonClient, error) { return &fakeBridgeDaemonClient{}, nil } - bridgeSrv, err := aibridged.New(fakeDialer, "127.0.0.1:0", log, api.DeploymentValues.AI.Value.BridgeConfig) + bridgeSrv, err := aibridged.New(fakeDialer, "127.0.0.1:0", log, api.DeploymentValues.AI.BridgeConfig) require.NoError(t, err) api.AIBridgeDaemons = []*aibridged.Server{bridgeSrv} - // TODO: improve approach. + // Wait for bridge server to start listening require.Eventually(t, func() bool { - return len(api.AIBridgeDaemons) > 0 + return len(api.AIBridgeDaemons) > 0 && api.AIBridgeDaemons[0].BridgeAddr() != "" }, testutil.WaitMedium, testutil.IntervalFast) body := []byte(` diff --git a/aibridged/proxy.go b/aibridged/proxy.go index 2fec88df3fad3..a95b31b500d89 100644 --- a/aibridged/proxy.go +++ b/aibridged/proxy.go @@ -209,7 +209,7 @@ func (sir *streamingInterceptReader) Read(p []byte) (n int, err error) { // Once [DONE] is found, we need to inject the trailer events and rewind the reader. if bytes.Contains(chunk, []byte(`[DONE]`)) { // TODO: inject trailers then [DONE] events. - //continue + // continue } extraEvents, send, interceptErr := sir.interceptFunc(sir.sess, chunk, true) @@ -374,7 +374,7 @@ func (p *SSEProxy) ServeHTTPWithContext(ctx context.Context, w http.ResponseWrit go func() { select { case <-ctx.Done(): - // Context cancelled, try to close the connection + // Context canceled, try to close the connection if hijacker, ok := w.(http.Hijacker); ok { if conn, _, err := hijacker.Hijack(); err == nil { conn.Close() diff --git a/aibridged/streaming.go b/aibridged/streaming.go index c4e840d82da97..ecc98bdc05528 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -1,14 +1,24 @@ package aibridged import ( + "bytes" + "context" + "fmt" "net/http" + "os" + "sync" + + "github.com/google/uuid" + "golang.org/x/xerrors" "cdr.dev/slog" + + "github.com/coder/coder/v2/aibridged/util" ) // BasicSSESender was implemented to overcome httpapi.ServerSentEventSender's odd design choices. For example, it doesn't // write "event: data" for every data event (it's unnecessary, and breaks some AI tools' parsing of the SSE stream). -func BasicSSESender(eventsChan <-chan string, logger slog.Logger) http.HandlerFunc { +func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, stream EventStreamer, logger slog.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -17,38 +27,125 @@ func BasicSSESender(eventsChan <-chan string, logger slog.Logger) http.HandlerFu w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "SSE not supported", http.StatusInternalServerError) - return - } - // Send initial flush to ensure connection is established. - flusher.Flush() + flush(w) for { select { + case <-outerCtx.Done(): + return case <-ctx.Done(): return - case event, ok := <-eventsChan: + case <-stream.Closed(): + return + case payload, ok := <-stream.Events(): if !ok { - // Channel closed, send done event and exit - _, err := w.Write([]byte("data: [DONE]\n\n")) // Convention used by OpenAI. // TODO: others, too? - if err != nil { - logger.Error(ctx, "failed to write done event", slog.Error(err)) - } - flusher.Flush() return } - // Send data event - _, err := w.Write([]byte("data: " + event + "\n\n")) + var buf bytes.Buffer + + buf.Write([]byte("data: ")) + buf.Write(payload) + buf.Write([]byte("\n\n")) + + // TODO: use logger, make configurable. + _, _ = fmt.Fprintf(os.Stderr, "[%s] %s", sessionID, buf.Bytes()) + + _, err := w.Write(buf.Bytes()) if err != nil { logger.Error(ctx, "failed to write SSE event", slog.Error(err)) return } - flusher.Flush() + flush(w) } } } } + +func flush(w http.ResponseWriter) { + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "SSE not supported", http.StatusInternalServerError) + return + } + + if flusher == nil { + return + } + + flusher.Flush() +} + +type EventStreamer interface { + TrySend(ctx context.Context, data any, exclusions ...string) error + Events() <-chan []byte + Close(ctx context.Context) error + Closed() <-chan any +} + +type openAIEventStream struct { + eventsCh chan []byte + + closedOnce sync.Once + closedCh chan any +} + +func newOpenAIEventStream() *openAIEventStream { + return &openAIEventStream{ + eventsCh: make(chan []byte), + closedCh: make(chan any), + } +} + +func (s *openAIEventStream) Events() <-chan []byte { + return s.eventsCh +} + +func (s *openAIEventStream) Closed() <-chan any { + return s.closedCh +} + +func (s *openAIEventStream) TrySend(ctx context.Context, data any, exclusions ...string) error { + // Save an unnecessary marshaling if possible. + select { + case <-ctx.Done(): + return ctx.Err() + case <-s.closedCh: + return xerrors.New("closed") + default: + } + + payload, err := util.MarshalNoZero(data, exclusions...) + if err != nil { + return xerrors.Errorf("marshal payload: %w", err) + } + + return s.send(ctx, payload) +} + +func (s *openAIEventStream) send(ctx context.Context, payload []byte) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-s.closedCh: + return xerrors.New("closed") + case s.eventsCh <- payload: + return nil + } +} + +func (s *openAIEventStream) Close(ctx context.Context) error { + var out error + s.closedOnce.Do(func() { + err := s.send(ctx, []byte("[DONE]")) // TODO: OpenAI-specific? + if err != nil { + out = xerrors.Errorf("close stream: %w", err) + } + + close(s.closedCh) + close(s.eventsCh) + }) + + return out +} diff --git a/aibridged/util/zero_marshaler.go b/aibridged/util/zero_marshaler.go new file mode 100644 index 0000000000000..fd71b4b95b474 --- /dev/null +++ b/aibridged/util/zero_marshaler.go @@ -0,0 +1,309 @@ +package util + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/jmespath/go-jmespath" +) + +var ErrEmptyPath = errors.New("empty path provided") + +// MarshalNoZero serializes v to JSON, omitting any struct field whose entire +// value graph is zero **unless** the field carries a `no_omit` tag or matches +// one of the provided JMESPath exclusion expressions. +func MarshalNoZero(v any, exclusions ...string) ([]byte, error) { + cleaned, zero := prune(reflect.ValueOf(v), false, exclusions, "") + if zero { + return []byte("null"), nil + } + return json.Marshal(cleaned) +} + +// matchesExclusion checks if the current path matches any of the JMESPath exclusion patterns. +func matchesExclusion(path string, exclusions []string) bool { + if len(exclusions) == 0 || path == "" { + return false + } + + // Create a temporary object with the path structure to test against + // For example, if path is "user.profile.name", we create {"user":{"profile":{"name":true}}} + testObj, err := createTestObject(path) + if err != nil { + return false + } + + for _, exclusion := range exclusions { + result, err := jmespath.Search(exclusion, testObj) + if err != nil { + continue + } + if result != nil { + return true + } + } + return false +} + +// createTestObject creates a nested object structure from a dot-notation path +// for testing against JMESPath expressions. +func createTestObject(path string) (map[string]any, error) { + if path == "" { + return nil, ErrEmptyPath + } + + parts := strings.Split(path, ".") + result := make(map[string]any) + current := result + + for i, part := range parts { + // Handle array notation like "items[0]" + if strings.Contains(part, "[") && strings.Contains(part, "]") { + // Extract array name and index + arrayName := part[:strings.Index(part, "[")] + indexStr := part[strings.Index(part, "[")+1 : strings.Index(part, "]")] + + // Create array structure + var arr []any + if idx := parseArrayIndex(indexStr); idx >= 0 { + // Create array with enough elements + for j := 0; j <= idx; j++ { + arr = append(arr, make(map[string]any)) + } + current[arrayName] = arr + if i == len(parts)-1 { + arr[idx] = true // Mark as matched for final element + } else { + if nextMap, ok := arr[idx].(map[string]any); ok { + current = nextMap + } else { + return nil, fmt.Errorf("invalid path structure at %s", part) + } + } + } + } else { + if i == len(parts)-1 { + current[part] = true // Mark final element as matched + } else { + current[part] = make(map[string]any) + if nextMap, ok := current[part].(map[string]any); ok { + current = nextMap + } else { + return nil, fmt.Errorf("invalid path structure at %s", part) + } + } + } + } + + return result, nil +} + +// parseArrayIndex extracts numeric index from array notation, returns -1 if invalid. +func parseArrayIndex(indexStr string) int { + // Simple numeric parsing - extend if needed for more complex patterns + if indexStr == "*" { + return 0 // Treat wildcard as index 0 for testing + } + + var idx int + if _, err := fmt.Sscanf(indexStr, "%d", &idx); err == nil { + return idx + } + return -1 +} + +// prune walks the value tree and returns (cleanedValue, isZeroTree). +// `forceKeep` is true when an ancestor field has the `no_omit` tag or matches an exclusion. +// `exclusions` contains JMESPath expressions to exclude from zero-omission. +// `currentPath` is the current JSON path being processed. +func prune(v reflect.Value, forceKeep bool, exclusions []string, currentPath string) (any, bool) { + if !v.IsValid() { + return nil, true + } + + // Allow custom IsZero() overrides. + if v.CanInterface() { + if z, ok := v.Interface().(interface{ IsZero() bool }); ok && z.IsZero() && !forceKeep { + return nil, true + } + } + + switch v.Kind() { + case reflect.Pointer, reflect.Interface: + if v.IsNil() { + if forceKeep { + return nil, false + } + return nil, true + } + return prune(v.Elem(), forceKeep, exclusions, currentPath) + + case reflect.Struct: + out := make(map[string]any) + allZero := true + vt := v.Type() + + for i := 0; i < v.NumField(); i++ { + sf := vt.Field(i) + if sf.PkgPath != "" { // unexported + continue + } + + jName, jOpts := parseJSONTag(sf.Tag.Get("json")) + if jName == "-" { + continue + } + if jName == "" { + jName = sf.Name + } + + // Build path for this field + fieldPath := currentPath + if fieldPath == "" { + fieldPath = jName + } else { + fieldPath = fieldPath + "." + jName + } + + childForce := forceKeep || hasNoOmit(sf.Tag, jOpts) || matchesExclusion(fieldPath, exclusions) + val, zero := prune(v.Field(i), childForce, exclusions, fieldPath) + if zero && !childForce { + continue + } + allZero = false + out[jName] = val + } + + if allZero && !forceKeep { + return nil, true + } + return out, false + + case reflect.Slice, reflect.Array: + if v.Len() == 0 && !forceKeep { + return nil, true + } + arr := make([]any, 0, v.Len()) + allZero := true + for i := 0; i < v.Len(); i++ { + // Build path for array element + elementPath := fmt.Sprintf("%s[%d]", currentPath, i) + if currentPath == "" { + elementPath = fmt.Sprintf("[%d]", i) + } + + elementForce := matchesExclusion(elementPath, exclusions) + val, zero := prune(v.Index(i), elementForce, exclusions, elementPath) + arr = append(arr, val) + if !zero { + allZero = false + } + } + if allZero && !forceKeep { + return nil, true + } + return arr, false + + case reflect.Map: + if v.Len() == 0 && !forceKeep { + return nil, true + } + m := make(map[string]any) + allZero := true + for _, k := range v.MapKeys() { + if k.Kind() != reflect.String { // JSON maps need string keys + continue + } + + // Build path for map key + keyPath := currentPath + if keyPath == "" { + keyPath = k.String() + } else { + keyPath = keyPath + "." + k.String() + } + + keyForce := matchesExclusion(keyPath, exclusions) + val, zero := prune(v.MapIndex(k), keyForce, exclusions, keyPath) + if zero && !keyForce { + continue + } + allZero = false + m[k.String()] = val + } + if allZero && !forceKeep { + return nil, true + } + return m, false + + case reflect.Bool: + if !v.Bool() && !forceKeep { + return nil, true + } + return v.Bool(), false + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if v.Int() == 0 && !forceKeep { + return nil, true + } + return v.Int(), false + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if v.Uint() == 0 && !forceKeep { + return nil, true + } + return v.Uint(), false + case reflect.Float32, reflect.Float64: + if v.Float() == 0 && !forceKeep { + return nil, true + } + return v.Float(), false + case reflect.String: + if v.Len() == 0 && !forceKeep { + return nil, true + } + return v.String(), false + default: // chan, func, unsafe pointers, etc. + return v.Interface(), false + } +} + +// ---------------- tag parsing ---------------- + +func parseJSONTag(tag string) (name string, opts tagOpts) { + if idx := strings.Index(tag, ","); idx != -1 { + return tag[:idx], tagOpts(tag[idx+1:]) + } + return tag, tagOpts("") +} + +type tagOpts string + +func (o tagOpts) contains(opt string) bool { + if len(o) == 0 { + return false + } + s := string(o) + for s != "" { + var next string + i := strings.Index(s, ",") + if i >= 0 { + s, next = s[:i], s[i+1:] + } + if s == opt { + return true + } + s = next + } + return false +} + +// hasNoOmit returns true if either a dedicated no_omit tag exists +// OR the json tag contains ",no_omit". +func hasNoOmit(tag reflect.StructTag, jOpts tagOpts) bool { + if tag.Get("no_omit") != "" { + return true + } + return jOpts.contains("no_omit") +} diff --git a/aibridged/util/zero_marshaler_test.go b/aibridged/util/zero_marshaler_test.go new file mode 100644 index 0000000000000..c388a22baef79 --- /dev/null +++ b/aibridged/util/zero_marshaler_test.go @@ -0,0 +1,385 @@ +package util + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +type TestStruct struct { + Name string `json:"name"` + Age int `json:"age"` + Profile *ProfileStruct `json:"profile"` + Items []string `json:"items"` + Meta map[string]string `json:"meta"` + Active bool `json:"active"` + Score float64 `json:"score"` +} + +type ProfileStruct struct { + Bio string `json:"bio"` + Website string `json:"website"` +} + +func TestMarshalNoZero_BasicFunctionality(t *testing.T) { + t.Parallel() + // Test basic zero omission without exclusions + data := TestStruct{ + Name: "John", + // Age: 0 (zero value, should be omitted) + // Profile: nil (zero value, should be omitted) + Items: []string{}, // empty slice (zero value, should be omitted) + // Meta: nil (zero value, should be omitted) + // Active: false (zero value, should be omitted) + // Score: 0.0 (zero value, should be omitted) + } + + result, err := MarshalNoZero(data) + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + // Should only contain non-zero fields + require.Equal(t, "John", unmarshaled["name"]) + require.NotContains(t, unmarshaled, "age") + require.NotContains(t, unmarshaled, "profile") + require.NotContains(t, unmarshaled, "items") + require.NotContains(t, unmarshaled, "meta") + require.NotContains(t, unmarshaled, "active") + require.NotContains(t, unmarshaled, "score") +} + +func TestMarshalNoZero_WithNoOmitTag(t *testing.T) { + t.Parallel() + type TestStructWithTag struct { + Name string `json:"name"` + Age int `json:"age,no_omit"` + } + + data := TestStructWithTag{ + Name: "John", + Age: 0, // Should be included due to no_omit tag + } + + result, err := MarshalNoZero(data) + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + require.Equal(t, "John", unmarshaled["name"]) + require.Equal(t, float64(0), unmarshaled["age"]) // JSON numbers are float64 +} + +func TestMarshalNoZero_WithJMESPathExclusions(t *testing.T) { + t.Parallel() + data := TestStruct{ + Name: "John", + Age: 0, // Should be included due to exclusion + Score: 0.0, // Should be included due to exclusion + Active: false, // Should be included due to exclusion + Profile: &ProfileStruct{ + Bio: "", // Should be included due to exclusion + Website: "", // Should be omitted (not in exclusion) + }, + Items: []string{}, // Should be included due to exclusion + Meta: map[string]string{}, // Should be omitted (not in exclusion) + } + + result, err := MarshalNoZero(data, "age", "score", "active", "profile.bio", "items") + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + require.Equal(t, "John", unmarshaled["name"]) + require.Equal(t, float64(0), unmarshaled["age"]) + require.Equal(t, float64(0), unmarshaled["score"]) + require.Equal(t, false, unmarshaled["active"]) + require.Contains(t, unmarshaled, "items") + require.NotContains(t, unmarshaled, "meta") + + // Check nested profile + profile := unmarshaled["profile"].(map[string]interface{}) + require.Equal(t, "", profile["bio"]) + require.NotContains(t, profile, "website") +} + +func TestMarshalNoZero_ArrayExclusions(t *testing.T) { + t.Parallel() + type ArrayTest struct { + Items []TestStruct `json:"items"` + } + + data := ArrayTest{ + Items: []TestStruct{ + {Name: "John", Age: 0}, // Age should be omitted normally + {Name: "", Age: 25}, // Name should be omitted normally + }, + } + + // Test excluding specific array elements + result, err := MarshalNoZero(data, "items[0].age", "items[1].name") + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + items := unmarshaled["items"].([]interface{}) + require.Len(t, items, 2) + + // First item should have age included + item0 := items[0].(map[string]interface{}) + require.Equal(t, "John", item0["name"]) + require.Equal(t, float64(0), item0["age"]) + + // Second item should have name included + item1 := items[1].(map[string]interface{}) + require.Equal(t, "", item1["name"]) + require.Equal(t, float64(25), item1["age"]) +} + +func TestMarshalNoZero_WildcardExclusions(t *testing.T) { + t.Parallel() + type WildcardTest struct { + Users []TestStruct `json:"users"` + } + + data := WildcardTest{ + Users: []TestStruct{ + {Name: "John", Age: 0}, + {Name: "Jane", Age: 0}, + }, + } + + // Test wildcard exclusion + result, err := MarshalNoZero(data, "users[*].age") + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + users := unmarshaled["users"].([]interface{}) + require.Len(t, users, 2) + + // Both users should have age included + user0 := users[0].(map[string]interface{}) + require.Equal(t, "John", user0["name"]) + require.Equal(t, float64(0), user0["age"]) + + user1 := users[1].(map[string]interface{}) + require.Equal(t, "Jane", user1["name"]) + require.Equal(t, float64(0), user1["age"]) +} + +func TestMarshalNoZero_MapExclusions(t *testing.T) { + t.Parallel() + data := TestStruct{ + Name: "John", + Meta: map[string]string{ + "key1": "", // Should be omitted normally + "key2": "value", + }, + } + + result, err := MarshalNoZero(data, "meta.key1") + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + require.Equal(t, "John", unmarshaled["name"]) + meta := unmarshaled["meta"].(map[string]interface{}) + require.Equal(t, "", meta["key1"]) // Should be included due to exclusion + require.Equal(t, "value", meta["key2"]) +} + +func TestMarshalNoZero_ComplexNestedExclusions(t *testing.T) { + t.Parallel() + type NestedTest struct { + Level1 struct { + Level2 struct { + Value string `json:"value"` + Count int `json:"count"` + } `json:"level2"` + Items []struct { + Name string `json:"name"` + ID int `json:"id"` + } `json:"items"` + } `json:"level1"` + } + + data := NestedTest{} + data.Level1.Level2.Value = "" // Should be included due to exclusion + data.Level1.Level2.Count = 0 // Should be omitted + data.Level1.Items = []struct { + Name string `json:"name"` + ID int `json:"id"` + }{ + {Name: "", ID: 0}, // Name should be included due to exclusion + } + + result, err := MarshalNoZero(data, "level1.level2.value", "level1.items[0].name") + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + level1 := unmarshaled["level1"].(map[string]interface{}) + level2 := level1["level2"].(map[string]interface{}) + require.Equal(t, "", level2["value"]) + require.NotContains(t, level2, "count") + + items := level1["items"].([]interface{}) + require.Len(t, items, 1) + item0 := items[0].(map[string]interface{}) + require.Equal(t, "", item0["name"]) + require.NotContains(t, item0, "id") +} + +func TestMarshalNoZero_EmptyExclusions(t *testing.T) { + t.Parallel() + data := TestStruct{ + Name: "John", + Age: 0, + } + + // Test with empty exclusions array + result, err := MarshalNoZero(data, []string{}...) + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + require.Equal(t, "John", unmarshaled["name"]) + require.NotContains(t, unmarshaled, "age") +} + +func TestMarshalNoZero_InvalidJMESPath(t *testing.T) { + t.Parallel() + data := TestStruct{ + Name: "John", + Age: 0, + } + + // Test with invalid JMESPath - should not crash, just ignore invalid patterns + result, err := MarshalNoZero(data, "invalid[[[path", "age") + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + require.Equal(t, "John", unmarshaled["name"]) + require.Equal(t, float64(0), unmarshaled["age"]) // Should still be included due to valid "age" exclusion +} + +func TestMarshalNoZero_BackwardCompatibility(t *testing.T) { + t.Parallel() + data := TestStruct{ + Name: "John", + Age: 0, + } + + // Test calling without any exclusions (backward compatibility) + result, err := MarshalNoZero(data) + require.NoError(t, err) + + var unmarshaled map[string]interface{} + err = json.Unmarshal(result, &unmarshaled) + require.NoError(t, err) + + require.Equal(t, "John", unmarshaled["name"]) + require.NotContains(t, unmarshaled, "age") +} + +func TestCreateTestObject(t *testing.T) { + t.Parallel() + tests := []struct { + path string + expected map[string]interface{} + hasError bool + }{ + { + path: "user.name", + expected: map[string]interface{}{ + "user": map[string]interface{}{ + "name": true, + }, + }, + hasError: false, + }, + { + path: "items[0].name", + expected: map[string]interface{}{ + "items": []interface{}{ + map[string]interface{}{ + "name": true, + }, + }, + }, + hasError: false, + }, + { + path: "simple", + expected: map[string]interface{}{ + "simple": true, + }, + hasError: false, + }, + { + path: "", + expected: nil, + hasError: true, + }, + } + + for _, test := range tests { + t.Run(test.path, func(t *testing.T) { + t.Parallel() + result, err := createTestObject(test.path) + if test.hasError { + require.Error(t, err) + require.Nil(t, result) + } else { + require.NoError(t, err) + require.Equal(t, test.expected, result) + } + }) + } +} + +func TestMatchesExclusion(t *testing.T) { + t.Parallel() + tests := []struct { + path string + exclusions []string + expected bool + }{ + {"user.name", []string{"user.name"}, true}, + {"user.age", []string{"user.name"}, false}, + {"items[0].name", []string{"items[0].name"}, true}, + {"items[1].name", []string{"items[0].name"}, false}, + {"user.profile.bio", []string{"user.profile.*"}, true}, + {"", []string{"user.name"}, false}, + {"user.name", []string{}, false}, + } + + for _, test := range tests { + t.Run(test.path, func(t *testing.T) { + t.Parallel() + result := matchesExclusion(test.path, test.exclusions) + require.Equal(t, test.expected, result) + }) + } +} diff --git a/cli/server.go b/cli/server.go index b90a6267e8040..5fa9328bf7f36 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1246,7 +1246,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } wg.Wait() - // Shut down aibridge daemons before waiting for WebSockets // connections to close. for i, aiBridgeDaemon := range aiBridgeDaemons { @@ -1260,7 +1259,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. err := shutdownWithTimeout(func(ctx context.Context) error { // We only want to cancel active jobs if we aren't exiting gracefully. return aiBridgeDaemon.Shutdown(ctx) - }, 5 * time.Second) + }, 5*time.Second) if err != nil { cliui.Errorf(inv.Stderr, "Failed to shut down AI bridge daemon %d: %s\n", id, err) return diff --git a/coderd/aibridge.go b/coderd/aibridge.go index b96d6bedd4a5e..0f4aeadd8e804 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -16,7 +16,7 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { // Something, somewhere is adding a duplicate header. // Haven't been able to track it down yet. - //rw.Header().Del("Access-Control-Allow-Origin") + // rw.Header().Del("Access-Control-Allow-Origin") if len(api.AIBridgeDaemons) == 0 { http.Error(rw, "no AI bridge daemons running", http.StatusInternalServerError) diff --git a/coderd/coderd.go b/coderd/coderd.go index bc0a141fc36f8..2b0996a3281a1 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1949,7 +1949,7 @@ func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name strin } //// TODO: naming... -//func (api *API) CreateInMemoryOpenAIBridgeClient(dialCtx context.Context, srv *aibridged.Server) (client aibridgedproto.DRPCOpenAIServiceClient, err error) { +// func (api *API) CreateInMemoryOpenAIBridgeClient(dialCtx context.Context, srv *aibridged.Server) (client aibridgedproto.DRPCOpenAIServiceClient, err error) { // // TODO(dannyk): implement options. // // TODO(dannyk): implement tracing. // diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 1465d94a44eb6..bed8e81038176 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -399,7 +399,7 @@ type DeploymentValues struct { WorkspaceHostnameSuffix serpent.String `json:"workspace_hostname_suffix,omitempty" typescript:",notnull"` Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"` HideAITasks serpent.Bool `json:"hide_ai_tasks,omitempty" typescript:",notnull"` - AI AIConfig `json:"ai,omitempty"` + AI AIConfig `json:"ai,omitempty"` Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"` WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"` diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index a8ee8a6d2de16..24433c1b2a6da 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -7,10 +7,11 @@ import ( "encoding/json" "io" - "github.com/coder/aisdk-go" "github.com/google/uuid" "golang.org/x/xerrors" + "github.com/coder/aisdk-go" + "github.com/coder/coder/v2/codersdk" ) diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index 3db1e61589465..d08191a614a99 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -9,12 +9,13 @@ import ( "testing" "time" - "github.com/coder/aisdk-go" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" + "github.com/coder/aisdk-go" + "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbfake" diff --git a/go.mod b/go.mod index d8b5b9e548d69..ad3cabebef018 100644 --- a/go.mod +++ b/go.mod @@ -487,7 +487,7 @@ require ( github.com/coder/preview v1.0.1 github.com/fsnotify/fsnotify v1.9.0 github.com/mark3labs/mcp-go v0.32.0 - github.com/openai/openai-go v1.7.0 + github.com/openai/openai-go v1.8.1 ) require ( diff --git a/go.sum b/go.sum index de37a9e064759..917867700cdec 100644 --- a/go.sum +++ b/go.sum @@ -1613,8 +1613,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1/go.mod h1:01TvyaK8x640crO2iFwW/6CFCZgNsOvOGH3B5J239m0= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1 h1:TCyOus9tym82PD1VYtthLKMVMlVyRwtDI4ck4SR2+Ok= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1/go.mod h1:Z/S1brD5gU2Ntht/bHxBVnGxXKTvZDr0dNv/riUzPmY= -github.com/openai/openai-go v1.7.0 h1:M1JfDjQgo3d3PsLyZgpGUG0wUAaUAitqJPM4Rl56dCA= -github.com/openai/openai-go v1.7.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/openai/openai-go v1.8.1 h1:mGS5Y9dEeHvLnE3k9LF4vUV3pvYG2K/6MHI/fCr4Ou8= +github.com/openai/openai-go v1.8.1/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= From 566900bf4abef7ac8e17aec502c80b151ff02d18 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 30 Jun 2025 15:49:01 +0200 Subject: [PATCH 19/61] chore: track OAI prompts Signed-off-by: Danny Kopping --- aibridged/bridge.go | 348 +++++--------------------------------------- 1 file changed, 39 insertions(+), 309 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 7f28e6312299f..b4ee8c7743e19 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -12,6 +12,7 @@ import ( "net/http/httputil" "net/url" "os" + "regexp" "strings" "sync" "time" @@ -21,7 +22,6 @@ import ( "github.com/google/uuid" "github.com/openai/openai-go" "github.com/openai/openai-go/packages/param" - openai_ssestream "github.com/openai/openai-go/packages/ssestream" "github.com/openai/openai-go/shared/constant" "github.com/tidwall/gjson" "golang.org/x/xerrors" @@ -45,7 +45,6 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli var bridge Bridge mux := &http.ServeMux{} - // mux.HandleFunc("/v1/chat/completions", bridge.proxyOpenAIRequestPrev) mux.HandleFunc("/v1/chat/completions", bridge.proxyOpenAIRequest) mux.HandleFunc("/v1/messages", bridge.proxyAnthropicRequest) @@ -99,82 +98,6 @@ func (b *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { return nil } -// type SSERoundTripper struct { -// transport http.RoundTripper -//} -// -// func (s *SSERoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { -// // Use default transport if none specified -// transport := s.transport -// if transport == nil { -// transport = &http.Transport{ -// DisableCompression: true, -// ResponseHeaderTimeout: 0, // No timeout for SSE -// IdleConnTimeout: 300 * time.Second, -// } -// } -// -// // Modify request for SSE -// req.Header.Set("Cache-Control", "no-cache") -// req.Header.Set("Accept", "text/event-stream") -// -// resp, err := transport.RoundTrip(req) -// if err != nil { -// return resp, err -// } -// -// resp.Body = wrapResponseBody(resp.Body) -// -// //var buf bytes.Buffer -// //teeReader := io.TeeReader(resp.Body, &buf) -// ////out, err := io.ReadAll(teeReader) -// ////if err != nil { -// //// return nil, xerrors.Errorf("intercept stream: %w", err) -// ////} -// // -// //newResp := &http.Response{ -// // Body: io.NopCloser(bytes.NewBuffer(buf.Bytes())), -// // Header: resp.Header, -// //} -// // -// //stream := openai_ssestream.NewStream[openai.ChatCompletionChunk](openai_ssestream.NewDecoder(newResp), nil) -// // -// //var msg openai.ChatCompletionAccumulator -// //for stream.Next() { -// // chunk := stream.Current() -// // msg.AddChunk(chunk) -// // -// // fmt.Println(chunk) -// //} -// -// return resp, err -//} -// -// func wrapResponseBody(body io.ReadCloser) io.ReadCloser { -// pr, pw := io.Pipe() -// go func() { -// defer pw.Close() -// defer body.Close() -// -// var buf bytes.Buffer -// teeReader := io.TeeReader(pr, &buf) -// -// // Read the entire stream first -// streamData, err := io.ReadAll(teeReader) -// if err != nil { -// return -// } -// -// // Write the original data to the pipe for the client -// go func() { -// defer pw.Close() -// pw.Write(streamData) -// }() -// }() -// -// return pr -//} - func (b *Bridge) openAITarget() *url.URL { u := b.cfg.OpenAIBaseURL.String() target, err := url.Parse(u) @@ -207,16 +130,12 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { defer cancel() r = r.WithContext(ctx) // Rewire context for SSE cancellation. - // Establish SSE stream which we will connect to requesting client. - // clientStream := NewSSEStream(eventsCh, b.logger.Named("sse-stream")) - coderdClient, ok := b.clientFn() if !ok { // TODO: log issue. http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) return } - _ = coderdClient // Parse incoming request, inject tool calls. var in ChatCompletionNewParamsWrapper @@ -234,6 +153,31 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { return } + if len(in.Messages) > 0 { + // Find last user message. + var msg *openai.ChatCompletionUserMessageParam + for i := len(in.Messages) - 1; i >= 0; i-- { + m := in.Messages[i] + if m.OfUser != nil { + msg = m.OfUser + break + } + } + + if msg != nil { + message := msg.Content.OfString.String() + if isCursor, _ := regexp.MatchString("", message); isCursor { + message = b.extractCursorUserQuery(message) + } + + if _, err = coderdClient.TrackUserPrompts(ctx, &proto.TrackUserPromptsRequest{ + Prompt: message, + }); err != nil { + b.logger.Error(r.Context(), "failed to track user prompt", slog.Error(err)) + } + } + } + // Prepend assistant message. in.Messages = append([]openai.ChatCompletionMessageParamUnion{ openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), @@ -378,40 +322,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { b.logger.Error(ctx, "failed to send tool chunk", slog.Error(err)) } - // type noOmitChoice struct { - // openai.ChatCompletionChunkChoice - // - // Delta openai.ChatCompletionChunkChoiceDelta `json:"delta,required,no_omit"` - //} - // - //type noOmitChunk struct { - // openai.ChatCompletionChunk - // Choices []noOmitChoice `json:"choices,required"` - //} - // - //finishChunk := noOmitChunk{ - // ChatCompletionChunk: openai.ChatCompletionChunk{ - // ID: acc.ID, - // Model: toolRes.Model, - // ServiceTier: openai.ChatCompletionChunkServiceTier(toolRes.ServiceTier), - // Created: time.Now().Unix(), - // SystemFingerprint: toolRes.SystemFingerprint, - // Usage: toolRes.Usage, - // Object: constant.ValueOf[constant.ChatCompletionChunk](), - // }, - // Choices: []noOmitChoice{ - // { - // ChatCompletionChunkChoice: openai.ChatCompletionChunkChoice{ - // FinishReason: string(openai.CompletionChoiceFinishReasonStop), - // }, - // Delta: openai.ChatCompletionChunkChoiceDelta{ - // //Role: "assistant", - // Content: "", - // }, - // }, - // }, - //} - finishChunk := openai.ChatCompletionChunk{ ID: acc.ID, Choices: []openai.ChatCompletionChunkChoice{ @@ -438,6 +348,10 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { continue } + // TODO: clean this up. Once we receive a tool invocation we need to hijack the conversation, since the client + // won't be handling the tool call if auto-injected. That means that any subsequent events which wrap + // up the stream need to be ignored because we send those after the tool call is executed and the result + // is appended as if it came from the assistant. if _, ok := ignoreSubsequent[acc.ID]; !ok { if err := eventStream.TrySend(streamCtx, chunk); err != nil { b.logger.Error(ctx, "failed to send reflected chunk", slog.Error(err)) @@ -477,201 +391,17 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } } -func (b *Bridge) proxyOpenAIRequestPrev(w http.ResponseWriter, r *http.Request) { - coderdClient, ok := b.clientFn() - if !ok { - // TODO: log issue. - http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) - return - } - - var acc openai.ChatCompletionAccumulator - proxy, err := NewSSEProxyWithConfig(ProxyConfig{ - OpenAISession: NewOpenAISession(), - Target: b.openAITarget(), - ModifyRequest: func(req *http.Request) { - var in ChatCompletionNewParamsWrapper - - body, err := io.ReadAll(req.Body) - if err != nil { - b.logger.Error(req.Context(), "failed to read body", slog.Error(err)) - http.Error(w, "failed to read body", http.StatusInternalServerError) - return - } - - if err = json.Unmarshal(body, &in); err != nil { - b.logger.Error(req.Context(), "failed to unmarshal request", slog.Error(err)) - http.Error(w, "failed to unmarshal request", http.StatusInternalServerError) - return - } - - in.Tools = []openai.ChatCompletionToolParam{ - { - Function: openai.FunctionDefinitionParam{ - Name: "get_weather", - Description: openai.String("Get weather at the given location"), - Parameters: openai.FunctionParameters{ - "type": "object", - "properties": map[string]interface{}{ - "location": map[string]string{ - "type": "string", - }, - }, - "required": []string{"location"}, - }, - }, - }, - } - - newBody, err := json.Marshal(in) - if err != nil { - b.logger.Error(req.Context(), "failed to marshal request", slog.Error(err)) - http.Error(w, "failed to marshal request", http.StatusInternalServerError) - return - } - - req.Body = io.NopCloser(bytes.NewReader(newBody)) - }, - RequestInterceptFunc: func(req *http.Request, body []byte) error { - var msg ChatCompletionNewParamsWrapper - err := json.NewDecoder(bytes.NewReader(body)).Decode(&msg) - if err != nil { - http.Error(w, "could not unmarshal request body", http.StatusBadRequest) - return xerrors.Errorf("unmarshal request body: %w", err) - } - // TODO: robustness - if len(msg.Messages) > 0 { - latest := msg.Messages[len(msg.Messages)-1] - if latest.OfUser != nil { - if latest.OfUser.Content.OfString.String() != "" { - _, _ = coderdClient.TrackUserPrompts(r.Context(), &proto.TrackUserPromptsRequest{ - Prompt: strings.TrimSpace(latest.OfUser.Content.OfString.String()), - }) - } - } - } - return nil - }, - ResponseInterceptFunc: func(session *OpenAISession, data []byte, isStreaming bool) ([][]byte, bool, error) { - b.logger.Info(r.Context(), "openai response received", slog.F("data", data), slog.F("streaming", isStreaming)) - - if !isStreaming { - return nil, true, nil - } - - response := &http.Response{ - Body: io.NopCloser(bytes.NewReader(data)), - } - stream := openai_ssestream.NewStream[openai.ChatCompletionChunk](openai_ssestream.NewDecoder(response), nil) - - var ( - inputToks, outputToks int64 - ) - for stream.Next() { - chunk := stream.Current() - - acc.AddChunk(chunk) - b.logger.Info(r.Context(), "openai chunk", slog.F("msgID", acc.ID), slog.F("contents", fmt.Sprintf("%+v", acc))) - - if acc.Usage.PromptTokens+acc.Usage.CompletionTokens > 0 { - inputToks = acc.Usage.PromptTokens - outputToks = acc.Usage.CompletionTokens - } - - var foundToolCallDelta bool - for _, c := range chunk.Choices { - for range c.Delta.ToolCalls { - foundToolCallDelta = true - - // Grab values from accumulator instead of delta. - for _, ac := range acc.ChatCompletion.Choices { - for _, at := range ac.Message.ToolCalls { - var ( - tc *OpenAIToolCall - ok bool - ) - if tc, ok = session.toolCallsRequired[at.ID]; !ok { - session.toolCallsRequired[at.ID] = &OpenAIToolCall{} - tc = session.toolCallsRequired[at.ID] - } - - session.toolCallsState[at.ID] = OpenAIToolCallNotReady - - tc.funcName = at.Function.Name - args := make(map[string]string) - err := json.Unmarshal([]byte(at.Function.Arguments), &args) - if err == nil { // Note: inverted. - tc.args = args - } - } - } - } - - // Once we receive a finish reason of "tool_calls", the API is waiting for the responses for this/these tool(s). - // We mark all the tool calls as ready. Once we see observe the [DONE] event, we will execute these tool calls. - if c.FinishReason == "tool_calls" { - for idx := range session.toolCallsState { - session.toolCallsState[idx] = OpenAIToolCallReady - } - } - } - - if foundToolCallDelta { - // Don't reflect these events back to client since they contain tool calls. - return nil, false, nil - } - } - if err := stream.Err(); err != nil { - panic(err) - } - - if inputToks+outputToks > 0 { - _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ - MsgId: acc.ID, - InputTokens: inputToks, - OutputTokens: outputToks, - }) - } - - if len(acc.Choices) < 0 { - return nil, true, nil - } - - var extra [][]byte - for idx, t := range session.toolCallsRequired { - // TODO: locking. - // TODO: index check. - session.toolCallsState[idx] = OpenAIToolCallInProgress - - fmt.Printf("EXEC TOOL! %s with %+v\n", t.funcName, t.args) - b, _ := json.Marshal(openai.ToolMessage("weather is rainy and cold in cape town today", idx)) // TODO: error handling. - extra = append(extra, b) - - session.toolCallsState[idx] = OpenAIToolCallDone - } - - return extra, true, nil - }, - }) - if err != nil { - b.logger.Error(r.Context(), "failed to create OpenAI proxy", slog.Error(err)) - http.Error(w, "failed to create OpenAI proxy", http.StatusInternalServerError) - return - } - - originalDirector := proxy.Director - proxy.Director = func(req *http.Request) { - originalDirector(req) - // Add OpenAI-specific headers - if strings.TrimSpace(req.Header.Get("Authorization")) == "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("OPENAI_API_KEY"))) +func (b *Bridge) extractCursorUserQuery(message string) string { + pat := regexp.MustCompile(`(?P[\s\S]*?)`) + match := pat.FindStringSubmatch(message) + if match != nil { + // Get the named group by index + contentIndex := pat.SubexpIndex("content") + if contentIndex != -1 { + message = match[contentIndex] } } - - proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - proxy.ServeHTTP(w, r) + return strings.TrimSpace(message) } func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { From 1d9fb5dffed6a4387fb92ca5755367d93ac5e728 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 1 Jul 2025 18:42:30 +0200 Subject: [PATCH 20/61] WIP: Anthropic proxy mostly working Signed-off-by: Danny Kopping --- aibridged/anthropic.go | 167 +++++++++++++++++++++++++++++++++++++++++ aibridged/bridge.go | 158 +++++++++++++++++++++++++++++++++++--- aibridged/streaming.go | 92 +++++++++++++++++------ 3 files changed, 386 insertions(+), 31 deletions(-) create mode 100644 aibridged/anthropic.go diff --git a/aibridged/anthropic.go b/aibridged/anthropic.go new file mode 100644 index 0000000000000..db3a30dad9f64 --- /dev/null +++ b/aibridged/anthropic.go @@ -0,0 +1,167 @@ +package aibridged + +import ( + "encoding/json" + + "github.com/anthropics/anthropic-sdk-go" + ant_param "github.com/anthropics/anthropic-sdk-go/packages/param" + "github.com/tidwall/gjson" +) + +type streamer interface { + UseStreaming() bool +} + +// ConvertStringContentToArrayTest exports the function for testing +func ConvertStringContentToArrayTest(raw []byte) ([]byte, error) { + return convertStringContentToArray(raw) +} + +// convertStringContentToArray converts string content to array format for Anthropic messages. +// https://docs.anthropic.com/en/api/messages#body-messages +// +// Each input message content may be either a single string or an array of content blocks, where each block has a +// specific type. Using a string for content is shorthand for an array of one content block of type "text". +// +func convertStringContentToArray(raw []byte) ([]byte, error) { + in := gjson.ParseBytes(raw) + + // Check if messages exist and need content conversion + if messages := in.Get("messages"); messages.Exists() { + var modifiedJSON map[string]interface{} + if err := json.Unmarshal(raw, &modifiedJSON); err != nil { + return raw, err + } + + convertStringContentRecursive(modifiedJSON) + + // Marshal back to JSON + return json.Marshal(modifiedJSON) + } + + return raw, nil +} + +// convertStringContentRecursive recursively scans JSON data and converts string "content" fields +// to proper text block arrays where needed for Anthropic SDK compatibility +func convertStringContentRecursive(data interface{}) { + switch v := data.(type) { + case map[string]interface{}: + // Check if this object has a "content" field with string value + if content, hasContent := v["content"]; hasContent { + if contentStr, isString := content.(string); isString { + // Check if this needs conversion based on context + if shouldConvertContentField(v) { + v["content"] = []map[string]interface{}{ + { + "type": "text", + "text": contentStr, + }, + } + } + } + } + + // Recursively process all values in the map + for _, value := range v { + convertStringContentRecursive(value) + } + + case []interface{}: + // Recursively process all items in the array + for _, item := range v { + convertStringContentRecursive(item) + } + } +} + +// shouldConvertContentField determines if a "content" string field should be converted to text block array +func shouldConvertContentField(obj map[string]interface{}) bool { + // Check if this is a message-level content (has "role" field) + if _, hasRole := obj["role"]; hasRole { + return true + } + + // Check if this is a tool_result block (but not mcp_tool_result which supports strings) + if objType, hasType := obj["type"].(string); hasType { + switch objType { + case "tool_result": + return true // Regular tool_result needs array format + case "mcp_tool_result": + return false // MCP tool_result supports strings + } + } + + return false +} + +// extractStreamFlag extracts the stream flag from JSON +func extractStreamFlag(raw []byte) bool { + in := gjson.ParseBytes(raw) + if streamVal := in.Get("stream"); streamVal.Exists() { + return streamVal.Bool() + } + return false +} + +// MessageNewParamsWrapper exists because the "stream" param is not included in anthropic.MessageNewParams. +type MessageNewParamsWrapper struct { + anthropic.MessageNewParams `json:""` + Stream bool `json:"stream,omitempty"` +} + +func (b MessageNewParamsWrapper) MarshalJSON() ([]byte, error) { + type shadow MessageNewParamsWrapper + return ant_param.MarshalWithExtras(b, (*shadow)(&b), map[string]any{ + "stream": b.Stream, + }) +} + +func (b *MessageNewParamsWrapper) UnmarshalJSON(raw []byte) error { + convertedRaw, err := convertStringContentToArray(raw) + if err != nil { + return err + } + + err = b.MessageNewParams.UnmarshalJSON(convertedRaw) + if err != nil { + return err + } + + b.Stream = extractStreamFlag(raw) + return nil +} +func (b *MessageNewParamsWrapper) UseStreaming() bool { + return b.Stream +} + +// BetaMessageNewParamsWrapper exists because the "stream" param is not included in anthropic.BetaMessageNewParams. +type BetaMessageNewParamsWrapper struct { + anthropic.BetaMessageNewParams `json:""` + Stream bool `json:"stream,omitempty"` +} + +func (b BetaMessageNewParamsWrapper) MarshalJSON() ([]byte, error) { + type shadow BetaMessageNewParamsWrapper + return ant_param.MarshalWithExtras(b, (*shadow)(&b), map[string]any{ + "stream": b.Stream, + }) +} + +func (b *BetaMessageNewParamsWrapper) UnmarshalJSON(raw []byte) error { + convertedRaw, err := convertStringContentToArray(raw) + if err != nil { + return err + } + + err = b.BetaMessageNewParams.UnmarshalJSON(convertedRaw) + if err != nil { + return err + } + + b.Stream = extractStreamFlag(raw) + return nil +} +func (b *BetaMessageNewParamsWrapper) UseStreaming() bool { + return b.Stream +} diff --git a/aibridged/bridge.go b/aibridged/bridge.go index b4ee8c7743e19..48675af31a35d 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -137,9 +137,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { return } - // Parse incoming request, inject tool calls. - var in ChatCompletionNewParamsWrapper - body, err := io.ReadAll(r.Body) if err != nil { b.logger.Error(r.Context(), "failed to read body", slog.Error(err)) @@ -147,6 +144,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { return } + var in ChatCompletionNewParamsWrapper if err = json.Unmarshal(body, &in); err != nil { b.logger.Error(r.Context(), "failed to unmarshal request", slog.Error(err)) http.Error(w, "failed to unmarshal request", http.StatusInternalServerError) @@ -207,7 +205,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { streamCtx, streamCancel := context.WithCancelCause(ctx) defer streamCancel(xerrors.New("deferred")) - eventStream := newOpenAIEventStream() + es := newEventStream(openAIEventStream) var wg sync.WaitGroup wg.Add(1) @@ -215,12 +213,12 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { go func() { defer wg.Done() defer func() { - if err := eventStream.Close(streamCtx); err != nil { + if err := es.Close(streamCtx); err != nil { b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("sessionID", sessionID)) } }() - BasicSSESender(streamCtx, sessionID, eventStream, b.logger.Named("sse-sender")).ServeHTTP(w, r) + BasicSSESender(streamCtx, sessionID, es, b.logger.Named("sse-sender")).ServeHTTP(w, r) }() session := NewOpenAISession() @@ -318,7 +316,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { Object: constant.ValueOf[constant.ChatCompletionChunk](), } - if err := eventStream.TrySend(streamCtx, toolChunk); err != nil { + if err := es.TrySend(streamCtx, toolChunk); err != nil { b.logger.Error(ctx, "failed to send tool chunk", slog.Error(err)) } @@ -341,7 +339,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { Object: constant.ValueOf[constant.ChatCompletionChunk](), } - if err := eventStream.TrySend(streamCtx, finishChunk, "choices[].delta.content"); err != nil { + if err := es.TrySend(streamCtx, finishChunk, "choices[].delta.content"); err != nil { b.logger.Error(ctx, "failed to send finish chunk", slog.Error(err)) } } @@ -353,13 +351,13 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // up the stream need to be ignored because we send those after the tool call is executed and the result // is appended as if it came from the assistant. if _, ok := ignoreSubsequent[acc.ID]; !ok { - if err := eventStream.TrySend(streamCtx, chunk); err != nil { + if err := es.TrySend(streamCtx, chunk); err != nil { b.logger.Error(ctx, "failed to send reflected chunk", slog.Error(err)) } } } - if err := eventStream.Close(streamCtx); err != nil { + if err := es.Close(streamCtx); err != nil { b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) } @@ -405,6 +403,146 @@ func (b *Bridge) extractCursorUserQuery(message string) string { } func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { + sessionID := uuid.New() + _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) + defer func() { + _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) + }() + + //out, _ := httputil.DumpRequest(r, true) + //fmt.Printf("\n\nREQUEST: %s\n\n", out) + + // Allow us to interrupt watch via cancel. + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + r = r.WithContext(ctx) // Rewire context for SSE cancellation. + + coderdClient, ok := b.clientFn() + if !ok { + // TODO: log issue. + http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) + return + } + _ = coderdClient + + useBeta := r.URL.Query().Get("beta") == "true" + if !useBeta { + http.Error(w, "only beta API supported", http.StatusInternalServerError) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + b.logger.Error(r.Context(), "failed to read body", slog.Error(err)) + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + //var in streamer + //if useBeta { + var in BetaMessageNewParamsWrapper + //} else { + // in = &MessageNewParamsWrapper{} + //} + + if err = json.Unmarshal(body, &in); err != nil { + b.logger.Error(r.Context(), "failed to unmarshal request", slog.Error(err)) + http.Error(w, "failed to unmarshal request", http.StatusInternalServerError) + return + } + + // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") + client := anthropic.NewClient() + if !in.UseStreaming() { + msg, err := client.Beta.Messages.New(ctx, in.BetaMessageNewParams) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + out := []byte(msg.RawJSON()) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(out) + return + } + + streamCtx, streamCancel := context.WithCancelCause(ctx) + defer streamCancel(xerrors.New("deferred")) + + es := newEventStream(anthropicEventStream) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + defer func() { + if err := es.Close(streamCtx); err != nil { + b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("sessionID", sessionID)) + } + }() + + BasicSSESender(streamCtx, sessionID, es, b.logger.Named("sse-sender")).ServeHTTP(w, r) + }() + + stream := client.Beta.Messages.NewStreaming(streamCtx, in.BetaMessageNewParams) + + var message anthropic.BetaMessage + for stream.Next() { + event := stream.Current() + + // Log MCP tool result events + eventData := event.RawJSON() + if strings.Contains(eventData, "web_search_requests") { + b.logger.Info(ctx, "Usage event with web_search_requests", slog.F("event", eventData)) + } + if strings.Contains(eventData, "mcp_tool_result") { + b.logger.Info(ctx, "MCP tool result event", slog.F("event", eventData)) + } + if strings.Contains(eventData, "tool_result") { + b.logger.Info(ctx, "Tool result event", slog.F("event", eventData)) + } + if strings.Contains(eventData, "tool_use") { + b.logger.Info(ctx, "Tool use event", slog.F("event", eventData)) + } + + if err := message.Accumulate(event); err != nil { + b.logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) + http.Error(w, "failed to proxy request", http.StatusInternalServerError) + return + } + + if err := es.TrySend(streamCtx, event); err != nil { + b.logger.Error(ctx, "failed to send event", slog.Error(err)) + } + } + + var streamErr error + if streamErr = stream.Err(); streamErr != nil { + http.Error(w, stream.Err().Error(), http.StatusInternalServerError) + } + + err = es.Close(streamCtx) + if err != nil { + b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) + } + + wg.Wait() + + // Ensure we flush all the remaining data before ending. + flush(w) + + if err != nil || streamErr != nil { + streamCancel(xerrors.Errorf("stream err: %w", err)) + } else { + streamCancel(xerrors.New("gracefully done")) + } + + select { + case <-streamCtx.Done(): + } +} + +func (b *Bridge) proxyAnthropicRequestPrev(w http.ResponseWriter, r *http.Request) { coderdClient, ok := b.clientFn() if !ok { // TODO: log issue. diff --git a/aibridged/streaming.go b/aibridged/streaming.go index ecc98bdc05528..6d2397683ccf6 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -3,7 +3,7 @@ package aibridged import ( "bytes" "context" - "fmt" + "encoding/json" "net/http" "os" "sync" @@ -43,16 +43,11 @@ func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, stream EventS return } - var buf bytes.Buffer - - buf.Write([]byte("data: ")) - buf.Write(payload) - buf.Write([]byte("\n\n")) - // TODO: use logger, make configurable. - _, _ = fmt.Fprintf(os.Stderr, "[%s] %s", sessionID, buf.Bytes()) + //_, _ = fmt.Fprintf(os.Stderr, "[%s] %s", sessionID, payload) + _, _ = os.Stderr.Write(payload) - _, err := w.Write(buf.Bytes()) + _, err := w.Write(payload) if err != nil { logger.Error(ctx, "failed to write SSE event", slog.Error(err)) return @@ -84,29 +79,38 @@ type EventStreamer interface { Closed() <-chan any } -type openAIEventStream struct { +type eventStream struct { eventsCh chan []byte + kind eventStreamProvider closedOnce sync.Once closedCh chan any } -func newOpenAIEventStream() *openAIEventStream { - return &openAIEventStream{ +type eventStreamProvider string + +const ( + openAIEventStream eventStreamProvider = "openai" + anthropicEventStream eventStreamProvider = "anthropic" +) + +func newEventStream(kind eventStreamProvider) *eventStream { + return &eventStream{ + kind: kind, eventsCh: make(chan []byte), closedCh: make(chan any), } } -func (s *openAIEventStream) Events() <-chan []byte { +func (s *eventStream) Events() <-chan []byte { return s.eventsCh } -func (s *openAIEventStream) Closed() <-chan any { +func (s *eventStream) Closed() <-chan any { return s.closedCh } -func (s *openAIEventStream) TrySend(ctx context.Context, data any, exclusions ...string) error { +func (s *eventStream) TrySend(ctx context.Context, data any, exclusions ...string) error { // Save an unnecessary marshaling if possible. select { case <-ctx.Done(): @@ -116,7 +120,21 @@ func (s *openAIEventStream) TrySend(ctx context.Context, data any, exclusions .. default: } - payload, err := util.MarshalNoZero(data, exclusions...) + var ( + payload []byte + err error + ) + switch s.kind { + case openAIEventStream: + // https://github.com/openai/openai-go#request-fields + // I noticed that Cursor would bork if it received streaming response payloads which had zero values. + // I'm not sure if this is a Cursor-specific issue or more widespread, but I've vibed a marshaler which will filter + // out all the zero value objects in the response, with optional exclusions. + payload, err = util.MarshalNoZero(data, exclusions...) + default: + payload, err = json.Marshal(data) + } + if err != nil { return xerrors.Errorf("marshal payload: %w", err) } @@ -124,7 +142,36 @@ func (s *openAIEventStream) TrySend(ctx context.Context, data any, exclusions .. return s.send(ctx, payload) } -func (s *openAIEventStream) send(ctx context.Context, payload []byte) error { +func (s *eventStream) send(ctx context.Context, payload []byte) error { + switch s.kind { + case openAIEventStream: + var buf bytes.Buffer + buf.WriteString("data: ") + buf.Write(payload) + buf.WriteString("\n\n") + payload = buf.Bytes() + case anthropicEventStream: + // TODO: improve this approach. + type msgType struct { + Val string `json:"type"` + } + var typ msgType + if err := json.NewDecoder(bytes.NewBuffer(payload)).Decode(&typ); err != nil { + return xerrors.Errorf("failed to determine anthropic event type for %q: %w", payload, err) + } + + var buf bytes.Buffer + buf.WriteString("event: ") + buf.WriteString(typ.Val) + buf.WriteString("\n") + buf.WriteString("data: ") + buf.Write(payload) + buf.WriteString("\n\n") + payload = buf.Bytes() + default: + return xerrors.Errorf("unknown stream kind: %q", s.kind) + } + select { case <-ctx.Done(): return ctx.Err() @@ -135,12 +182,15 @@ func (s *openAIEventStream) send(ctx context.Context, payload []byte) error { } } -func (s *openAIEventStream) Close(ctx context.Context) error { +func (s *eventStream) Close(ctx context.Context) error { var out error s.closedOnce.Do(func() { - err := s.send(ctx, []byte("[DONE]")) // TODO: OpenAI-specific? - if err != nil { - out = xerrors.Errorf("close stream: %w", err) + switch s.kind { + case openAIEventStream: + err := s.send(ctx, []byte("[DONE]")) + if err != nil { + out = xerrors.Errorf("close stream: %w", err) + } } close(s.closedCh) From 277989f2351213b6d2564990a8e7f1a821e83d16 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 2 Jul 2025 11:47:02 +0200 Subject: [PATCH 21/61] WIP: prompt & cost tracking Signed-off-by: Danny Kopping --- aibridged/bridge.go | 89 ++++++++++- aibridged/proto/aibridged.pb.go | 141 +++++++++++------- aibridged/proto/aibridged.proto | 3 + ...aibridge.up.sql => 000345_aibridge.up.sql} | 0 4 files changed, 178 insertions(+), 55 deletions(-) rename coderd/database/migrations/{000344_aibridge.up.sql => 000345_aibridge.up.sql} (100%) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 48675af31a35d..02a3c2547dea3 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -423,12 +423,13 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) return } - _ = coderdClient useBeta := r.URL.Query().Get("beta") == "true" if !useBeta { - http.Error(w, "only beta API supported", http.StatusInternalServerError) - return + b.logger.Warn(r.Context(), "non-beta API requested, using beta instead", slog.F("url", r.URL.String())) + useBeta = true + //http.Error(w, "only beta API supported", http.StatusInternalServerError) + //return } body, err := io.ReadAll(r.Body) @@ -451,6 +452,52 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { return } + // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. + isHaiku := strings.Contains(string(in.Model), "3-5-haiku") + + // Find the most recent user message and track the prompt. + if len(in.Messages) > 0 && !isHaiku { + var userMessage string + for i := len(in.Messages) - 1; i >= 0; i-- { + m := in.Messages[i] + if m.Role != anthropic.BetaMessageParamRoleUser { + continue + } + if len(m.Content) == 0 { + continue + } + + for j := len(m.Content) - 1; j >= 0; j-- { + if textContent := m.Content[j].GetText(); textContent != nil { + userMessage = *textContent + } + + // Ignore internal Claude Code prompts. + if userMessage == "test" || + strings.Contains(userMessage, "") { + userMessage = "" + continue + } + + // Handle Cursor-specific formatting by extracting content from tags + if isCursor, _ := regexp.MatchString("", userMessage); isCursor { + userMessage = b.extractCursorUserQuery(userMessage) + } + goto track + } + } + + track: + if userMessage != "" { + if _, err = coderdClient.TrackUserPrompts(ctx, &proto.TrackUserPromptsRequest{ + Prompt: userMessage, + Model: string(in.Model), + }); err != nil { + b.logger.Error(r.Context(), "failed to track user prompt", slog.Error(err)) + } + } + } + // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") client := anthropic.NewClient() if !in.UseStreaming() { @@ -459,6 +506,24 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) } + // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! + _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + MsgId: msg.ID, + Model: string(msg.Model), + InputTokens: msg.Usage.InputTokens, + OutputTokens: msg.Usage.OutputTokens, + Other: map[string]int64{ + "web_search_requests": msg.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": msg.Usage.CacheCreationInputTokens, + "cache_read_input": msg.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": msg.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": msg.Usage.CacheCreation.Ephemeral5mInputTokens, + }, + }) + if err != nil { + b.logger.Error(ctx, "failed to track usage", slog.Error(err)) + } + out := []byte(msg.RawJSON()) w.WriteHeader(http.StatusOK) _, _ = w.Write(out) @@ -516,6 +581,24 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } + // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! + _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ + MsgId: message.ID, + Model: string(message.Model), + InputTokens: message.Usage.InputTokens, + OutputTokens: message.Usage.OutputTokens, + Other: map[string]int64{ + "web_search_requests": message.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": message.Usage.CacheCreationInputTokens, + "cache_read_input": message.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, + }, + }) + if err != nil { + b.logger.Error(ctx, "failed to track usage", slog.Error(err)) + } + var streamErr error if streamErr = stream.Err(); streamErr != nil { http.Error(w, stream.Err().Error(), http.StatusInternalServerError) diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index 934ebc357941b..b500053e981d3 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -118,9 +118,11 @@ type TrackTokenUsageRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - MsgId string `protobuf:"bytes,1,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` - InputTokens int64 `protobuf:"varint,2,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` - OutputTokens int64 `protobuf:"varint,3,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` + MsgId string `protobuf:"bytes,1,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` + InputTokens int64 `protobuf:"varint,2,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` + OutputTokens int64 `protobuf:"varint,3,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` + Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` + Other map[string]int64 `protobuf:"bytes,5,rep,name=other,proto3" json:"other,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` } func (x *TrackTokenUsageRequest) Reset() { @@ -176,6 +178,20 @@ func (x *TrackTokenUsageRequest) GetOutputTokens() int64 { return 0 } +func (x *TrackTokenUsageRequest) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + +func (x *TrackTokenUsageRequest) GetOther() map[string]int64 { + if x != nil { + return x.Other + } + return nil +} + type TrackTokenUsageResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -220,6 +236,7 @@ type TrackUserPromptsRequest struct { unknownFields protoimpl.UnknownFields Prompt string `protobuf:"bytes,1,opt,name=prompt,proto3" json:"prompt,omitempty"` + Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` } func (x *TrackUserPromptsRequest) Reset() { @@ -261,6 +278,13 @@ func (x *TrackUserPromptsRequest) GetPrompt() string { return "" } +func (x *TrackUserPromptsRequest) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + type TrackUserPromptsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -310,42 +334,53 @@ var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x13, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x77, 0x0a, - 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, - 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x31, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, - 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, - 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, - 0x6f, 0x6d, 0x70, 0x74, 0x22, 0x1a, 0x0a, 0x18, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, - 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x32, 0x95, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, - 0x70, 0x74, 0x12, 0x1d, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, - 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, - 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, - 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, - 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x54, - 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x12, - 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, - 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8b, 0x02, + 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, + 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x42, 0x0a, + 0x05, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, + 0x4f, 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x6f, 0x74, 0x68, 0x65, + 0x72, 0x1a, 0x38, 0x0a, 0x0a, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x47, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, + 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, + 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x22, + 0x1a, 0x0a, 0x18, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, + 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x95, 0x02, 0x0a, 0x0e, + 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4c, + 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1d, 0x2e, + 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, + 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, + 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, + 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x61, 0x69, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, + 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, + 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, + 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, + 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -360,7 +395,7 @@ func file_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { return file_aibridged_proto_aibridged_proto_rawDescData } -var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*AuditPromptRequest)(nil), // 0: aibridged.AuditPromptRequest (*AuditPromptResponse)(nil), // 1: aibridged.AuditPromptResponse @@ -368,19 +403,21 @@ var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*TrackTokenUsageResponse)(nil), // 3: aibridged.TrackTokenUsageResponse (*TrackUserPromptsRequest)(nil), // 4: aibridged.TrackUserPromptsRequest (*TrackUserPromptsResponse)(nil), // 5: aibridged.TrackUserPromptsResponse + nil, // 6: aibridged.TrackTokenUsageRequest.OtherEntry } var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ - 0, // 0: aibridged.AIBridgeDaemon.AuditPrompt:input_type -> aibridged.AuditPromptRequest - 2, // 1: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest - 4, // 2: aibridged.AIBridgeDaemon.TrackUserPrompts:input_type -> aibridged.TrackUserPromptsRequest - 1, // 3: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse - 3, // 4: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse - 5, // 5: aibridged.AIBridgeDaemon.TrackUserPrompts:output_type -> aibridged.TrackUserPromptsResponse - 3, // [3:6] is the sub-list for method output_type - 0, // [0:3] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 6, // 0: aibridged.TrackTokenUsageRequest.other:type_name -> aibridged.TrackTokenUsageRequest.OtherEntry + 0, // 1: aibridged.AIBridgeDaemon.AuditPrompt:input_type -> aibridged.AuditPromptRequest + 2, // 2: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest + 4, // 3: aibridged.AIBridgeDaemon.TrackUserPrompts:input_type -> aibridged.TrackUserPromptsRequest + 1, // 4: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse + 3, // 5: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse + 5, // 6: aibridged.AIBridgeDaemon.TrackUserPrompts:output_type -> aibridged.TrackUserPromptsResponse + 4, // [4:7] is the sub-list for method output_type + 1, // [1:4] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_aibridged_proto_aibridged_proto_init() } @@ -468,7 +505,7 @@ func file_aibridged_proto_aibridged_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_aibridged_proto_aibridged_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index 1b6e045dc381e..a37e29d534f78 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -14,11 +14,14 @@ message TrackTokenUsageRequest { string msg_id = 1; int64 input_tokens = 2; int64 output_tokens = 3; + string model = 4; + map other = 5; } message TrackTokenUsageResponse {} message TrackUserPromptsRequest { string prompt = 1; + string model = 2; } message TrackUserPromptsResponse {} diff --git a/coderd/database/migrations/000344_aibridge.up.sql b/coderd/database/migrations/000345_aibridge.up.sql similarity index 100% rename from coderd/database/migrations/000344_aibridge.up.sql rename to coderd/database/migrations/000345_aibridge.up.sql From ab9dbe4ac077ae4f26eea30b4f94b9fe8c2f9b28 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 2 Jul 2025 15:13:15 +0200 Subject: [PATCH 22/61] chore: auth middleware Signed-off-by: Danny Kopping --- aibridged/bridge.go | 2 +- aibridged/middleware.go | 59 +++++++++++++++++++++++++++++++++++++++++ coderd/coderd.go | 1 + 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 aibridged/middleware.go diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 02a3c2547dea3..4ec1a2d557a64 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -452,7 +452,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { return } - // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. + // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). isHaiku := strings.Contains(string(in.Model), "3-5-haiku") // Find the most recent user message and track the prompt. diff --git a/aibridged/middleware.go b/aibridged/middleware.go new file mode 100644 index 0000000000000..077b8f1644c55 --- /dev/null +++ b/aibridged/middleware.go @@ -0,0 +1,59 @@ +package aibridged + +import ( + "bytes" + "crypto/subtle" + "net/http" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/httpmw" +) + +// AuthMiddleware extracts and validates authorization tokens for AI bridge endpoints. +// It supports both Bearer tokens in Authorization headers and Coder session tokens +// from cookies/headers following the same patterns as existing Coder authentication. +func AuthMiddleware(db database.Store) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Extract token using the same pattern as the bridge + token := extractAuthTokenForBridge(r) + if token == "" { + http.Error(rw, "Authorization token required", http.StatusUnauthorized) + return + } + + // Validate token using httpmw.APIKeyFromRequest + _, _, ok := httpmw.APIKeyFromRequest(ctx, db, func(r *http.Request) string { + return token + }, &http.Request{}) + + if !ok { + http.Error(rw, "Invalid or expired session token", http.StatusUnauthorized) + return + } + + next.ServeHTTP(rw, r) + }) + } +} + +// extractAuthTokenForBridge extracts authorization token from HTTP request using multiple sources. +// It checks Authorization header (Bearer token), Coder session headers, and cookies. +func extractAuthTokenForBridge(r *http.Request) string { + // 1. Check Authorization header for Bearer token + authHeader := r.Header.Get("Authorization") + if authHeader != "" { + bearer := []byte("bearer ") + hdr := []byte(authHeader) + + // Use case-insensitive comparison for Bearer token + if len(hdr) >= len(bearer) && subtle.ConstantTimeCompare(bytes.ToLower(hdr[:len(bearer)]), bearer) == 1 { + return string(hdr[len(bearer):]) + } + } + + // 2. Fall back to Coder's standard token extraction + return httpmw.APITokenFromRequest(r) +} diff --git a/coderd/coderd.go b/coderd/coderd.go index dc7a062b5e4ba..765737b9515f1 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1500,6 +1500,7 @@ func New(options *Options) *API { r.Get("/", api.tailnetRPCConn) }) r.Route("/aibridge", func(r chi.Router) { + r.Use(aibridged.AuthMiddleware(api.Database)) r.Post("/v1/chat/completions", api.bridgeAIRequest) r.Get("/v1/models", func(rw http.ResponseWriter, r *http.Request) { httpapi.Write(context.Background(), rw, http.StatusOK, map[string]any{ From 5de36a3a1a6d93620f4e32d0791112a7aca839e0 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 2 Jul 2025 16:44:47 +0200 Subject: [PATCH 23/61] track user prompts refactor Signed-off-by: Danny Kopping --- aibridged/bridge.go | 141 +++++++----------------------------------- aibridged/openai.go | 43 +++++++++++++ aibridged/tracking.go | 101 ++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 119 deletions(-) create mode 100644 aibridged/openai.go create mode 100644 aibridged/tracking.go diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 4ec1a2d557a64..cf65ba99e1122 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -12,7 +12,6 @@ import ( "net/http/httputil" "net/url" "os" - "regexp" "strings" "sync" "time" @@ -21,9 +20,7 @@ import ( ant_ssestream "github.com/anthropics/anthropic-sdk-go/packages/ssestream" "github.com/google/uuid" "github.com/openai/openai-go" - "github.com/openai/openai-go/packages/param" "github.com/openai/openai-go/shared/constant" - "github.com/tidwall/gjson" "golang.org/x/xerrors" "cdr.dev/slog" @@ -62,42 +59,6 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli return &bridge } -// ChatCompletionNewParamsWrapper exists because the "stream" param is not included in openai.ChatCompletionNewParams. -type ChatCompletionNewParamsWrapper struct { - openai.ChatCompletionNewParams `json:""` - Stream bool `json:"stream,omitempty"` -} - -func (b ChatCompletionNewParamsWrapper) MarshalJSON() ([]byte, error) { - type shadow ChatCompletionNewParamsWrapper - return param.MarshalWithExtras(b, (*shadow)(&b), map[string]any{ - "stream": b.Stream, - }) -} - -func (b *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { - err := b.ChatCompletionNewParams.UnmarshalJSON(raw) - if err != nil { - return err - } - - in := gjson.ParseBytes(raw) - if stream := in.Get("stream"); stream.Exists() { - b.Stream = stream.Bool() - if b.Stream { - b.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{ - IncludeUsage: openai.Bool(true), // Always include usage when streaming. - } - } else { - b.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} - } - } else { - b.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} - } - - return nil -} - func (b *Bridge) openAITarget() *url.URL { u := b.cfg.OpenAIBaseURL.String() target, err := url.Parse(u) @@ -151,28 +112,13 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { return } - if len(in.Messages) > 0 { - // Find last user message. - var msg *openai.ChatCompletionUserMessageParam - for i := len(in.Messages) - 1; i >= 0; i-- { - m := in.Messages[i] - if m.OfUser != nil { - msg = m.OfUser - break - } - } - - if msg != nil { - message := msg.Content.OfString.String() - if isCursor, _ := regexp.MatchString("", message); isCursor { - message = b.extractCursorUserQuery(message) - } - - if _, err = coderdClient.TrackUserPrompts(ctx, &proto.TrackUserPromptsRequest{ - Prompt: message, - }); err != nil { - b.logger.Error(r.Context(), "failed to track user prompt", slog.Error(err)) - } + prompt, err := in.LastUserPrompt() // TODO: error handling. + if prompt != nil { + if _, err = coderdClient.TrackUserPrompts(ctx, &proto.TrackUserPromptsRequest{ + Model: in.Model, + Prompt: *prompt, + }); err != nil { + b.logger.Error(r.Context(), "failed to track user prompt", slog.Error(err)) } } @@ -389,19 +335,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } } -func (b *Bridge) extractCursorUserQuery(message string) string { - pat := regexp.MustCompile(`(?P[\s\S]*?)`) - match := pat.FindStringSubmatch(message) - if match != nil { - // Get the named group by index - contentIndex := pat.SubexpIndex("content") - if contentIndex != -1 { - message = match[contentIndex] - } - } - return strings.TrimSpace(message) -} - func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { sessionID := uuid.New() _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) @@ -453,44 +386,14 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). - isHaiku := strings.Contains(string(in.Model), "3-5-haiku") + isSmallFastModel := strings.Contains(string(in.Model), "3-5-haiku") // Find the most recent user message and track the prompt. - if len(in.Messages) > 0 && !isHaiku { - var userMessage string - for i := len(in.Messages) - 1; i >= 0; i-- { - m := in.Messages[i] - if m.Role != anthropic.BetaMessageParamRoleUser { - continue - } - if len(m.Content) == 0 { - continue - } - - for j := len(m.Content) - 1; j >= 0; j-- { - if textContent := m.Content[j].GetText(); textContent != nil { - userMessage = *textContent - } - - // Ignore internal Claude Code prompts. - if userMessage == "test" || - strings.Contains(userMessage, "") { - userMessage = "" - continue - } - - // Handle Cursor-specific formatting by extracting content from tags - if isCursor, _ := regexp.MatchString("", userMessage); isCursor { - userMessage = b.extractCursorUserQuery(userMessage) - } - goto track - } - } - - track: - if userMessage != "" { + if !isSmallFastModel { + prompt, err := in.LastUserPrompt() // TODO: error handling. + if prompt != nil { if _, err = coderdClient.TrackUserPrompts(ctx, &proto.TrackUserPromptsRequest{ - Prompt: userMessage, + Prompt: *prompt, Model: string(in.Model), }); err != nil { b.logger.Error(r.Context(), "failed to track user prompt", slog.Error(err)) @@ -513,11 +416,11 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { InputTokens: msg.Usage.InputTokens, OutputTokens: msg.Usage.OutputTokens, Other: map[string]int64{ - "web_search_requests": msg.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": msg.Usage.CacheCreationInputTokens, - "cache_read_input": msg.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": msg.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": msg.Usage.CacheCreation.Ephemeral5mInputTokens, + "web_search_requests": msg.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": msg.Usage.CacheCreationInputTokens, + "cache_read_input": msg.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": msg.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": msg.Usage.CacheCreation.Ephemeral5mInputTokens, }, }) if err != nil { @@ -588,11 +491,11 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { InputTokens: message.Usage.InputTokens, OutputTokens: message.Usage.OutputTokens, Other: map[string]int64{ - "web_search_requests": message.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": message.Usage.CacheCreationInputTokens, - "cache_read_input": message.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, + "web_search_requests": message.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": message.Usage.CacheCreationInputTokens, + "cache_read_input": message.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, }, }) if err != nil { diff --git a/aibridged/openai.go b/aibridged/openai.go new file mode 100644 index 0000000000000..aafcf95733376 --- /dev/null +++ b/aibridged/openai.go @@ -0,0 +1,43 @@ +package aibridged + +import ( + "github.com/openai/openai-go" + "github.com/openai/openai-go/packages/param" + "github.com/tidwall/gjson" +) + +// ChatCompletionNewParamsWrapper exists because the "stream" param is not included in openai.ChatCompletionNewParams. +type ChatCompletionNewParamsWrapper struct { + openai.ChatCompletionNewParams `json:""` + Stream bool `json:"stream,omitempty"` +} + +func (c ChatCompletionNewParamsWrapper) MarshalJSON() ([]byte, error) { + type shadow ChatCompletionNewParamsWrapper + return param.MarshalWithExtras(c, (*shadow)(&c), map[string]any{ + "stream": c.Stream, + }) +} + +func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { + err := c.ChatCompletionNewParams.UnmarshalJSON(raw) + if err != nil { + return err + } + + in := gjson.ParseBytes(raw) + if stream := in.Get("stream"); stream.Exists() { + c.Stream = stream.Bool() + if c.Stream { + c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{ + IncludeUsage: openai.Bool(true), // Always include usage when streaming. + } + } else { + c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} + } + } else { + c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} + } + + return nil +} diff --git a/aibridged/tracking.go b/aibridged/tracking.go new file mode 100644 index 0000000000000..e22692c7fd3b7 --- /dev/null +++ b/aibridged/tracking.go @@ -0,0 +1,101 @@ +package aibridged + +import ( + "regexp" + "strings" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/openai/openai-go" + "golang.org/x/xerrors" + "tailscale.com/types/ptr" +) + +type UsageExtractor interface { + LastUserPrompt() (*string, error) + LastToolCalls() ([]string, error) +} + +func (c *ChatCompletionNewParamsWrapper) LastUserPrompt() (*string, error) { + if c == nil { + return nil, xerrors.New("nil struct") + } + + if len(c.Messages) == 0 { + return nil, xerrors.New("no messages") + } + + var msg *openai.ChatCompletionUserMessageParam + for i := len(c.Messages) - 1; i >= 0; i-- { + m := c.Messages[i] + if m.OfUser != nil { + msg = m.OfUser + break + } + } + + if msg == nil { + return nil, nil + } + + userMessage := msg.Content.OfString.String() + if isCursor, _ := regexp.MatchString("", userMessage); isCursor { + userMessage = extractCursorUserQuery(userMessage) + } + + return ptr.To(strings.TrimSpace(userMessage)), nil +} + +func (b *BetaMessageNewParamsWrapper) LastUserPrompt() (*string, error) { + if b == nil { + return nil, xerrors.New("nil struct") + } + + if len(b.Messages) == 0 { + return nil, xerrors.New("no messages") + } + + var userMessage string + for i := len(b.Messages) - 1; i >= 0; i-- { + m := b.Messages[i] + if m.Role != anthropic.BetaMessageParamRoleUser { + continue + } + if len(m.Content) == 0 { + continue + } + + for j := len(m.Content) - 1; j >= 0; j-- { + if textContent := m.Content[j].GetText(); textContent != nil { + userMessage = *textContent + } + + // Ignore internal Claude Code prompts. + if userMessage == "test" || + strings.Contains(userMessage, "") { + userMessage = "" + continue + } + + // Handle Cursor-specific formatting by extracting content from tags + if isCursor, _ := regexp.MatchString("", userMessage); isCursor { + userMessage = extractCursorUserQuery(userMessage) + } + return ptr.To(strings.TrimSpace(userMessage)), nil + } + } + + return nil, nil +} + +func extractCursorUserQuery(message string) string { + pat := regexp.MustCompile(`(?P[\s\S]*?)`) + match := pat.FindStringSubmatch(message) + if match != nil { + // Get the named group by index + contentIndex := pat.SubexpIndex("content") + if contentIndex != -1 { + message = match[contentIndex] + } + } + return strings.TrimSpace(message) +} From cd5ac5446ae4d0f9a33efdd85fe3d52233be6186 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 3 Jul 2025 16:13:54 +0200 Subject: [PATCH 24/61] WIP: tool calling & tracking Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 10 + aibridged/bridge.go | 154 ++++++++++-- aibridged/middleware.go | 10 +- aibridged/proto/aibridged.pb.go | 223 +++++++++++++++--- aibridged/proto/aibridged.proto | 8 + aibridged/proto/aibridged_drpc.pb.go | 42 +++- aibridged/streaming.go | 4 + coderd/aibridgedserver/aibridgedserver.go | 14 ++ ...aibridge.up.sql => 000347_aibridge.up.sql} | 0 go.mod | 6 +- go.sum | 8 + 11 files changed, 421 insertions(+), 58 deletions(-) rename coderd/database/migrations/{000345_aibridge.up.sql => 000347_aibridge.up.sql} (100%) diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index c45bd1089dce1..2d4ed97257217 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -188,6 +188,16 @@ func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPrompt return out, nil } +func (s *Server) TrackToolUse(ctx context.Context, in *proto.TrackToolUseRequest) (*proto.TrackToolUseResponse, error) { + out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackToolUseResponse, error) { + return client.TrackToolUse(ctx, in) + }) + if err != nil { + return nil, err + } + return out, nil +} + // func (s *Server) ChatCompletions(payload *proto.JSONPayload, stream proto.DRPCOpenAIService_ChatCompletionsStream) error { // // TODO: call OpenAI API. // diff --git a/aibridged/bridge.go b/aibridged/bridge.go index cf65ba99e1122..04d024ac04479 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -18,6 +18,7 @@ import ( "github.com/anthropics/anthropic-sdk-go" ant_ssestream "github.com/anthropics/anthropic-sdk-go/packages/ssestream" + ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" "github.com/google/uuid" "github.com/openai/openai-go" "github.com/openai/openai-go/shared/constant" @@ -27,6 +28,8 @@ import ( "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/codersdk" + + "github.com/invopop/jsonschema" ) type Bridge struct { @@ -385,6 +388,19 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { return } + toolParams := []anthropic.BetaToolParam{ + { + Name: "get_coordinates", + Description: anthropic.String("Accepts a place as an address, then returns the latitude and longitude coordinates."), + InputSchema: GetCoordinatesInputSchema, + }, + } + tools := make([]anthropic.BetaToolUnionParam, len(toolParams)) + for i, toolParam := range toolParams { + tools[i] = anthropic.BetaToolUnionParam{OfTool: &toolParam} + } + in.Tools = tools + // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). isSmallFastModel := strings.Contains(string(in.Model), "3-5-haiku") @@ -454,24 +470,13 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { stream := client.Beta.Messages.NewStreaming(streamCtx, in.BetaMessageNewParams) + var foundToolCall bool + + var events []anthropic.BetaRawMessageStreamEventUnion var message anthropic.BetaMessage for stream.Next() { event := stream.Current() - - // Log MCP tool result events - eventData := event.RawJSON() - if strings.Contains(eventData, "web_search_requests") { - b.logger.Info(ctx, "Usage event with web_search_requests", slog.F("event", eventData)) - } - if strings.Contains(eventData, "mcp_tool_result") { - b.logger.Info(ctx, "MCP tool result event", slog.F("event", eventData)) - } - if strings.Contains(eventData, "tool_result") { - b.logger.Info(ctx, "Tool result event", slog.F("event", eventData)) - } - if strings.Contains(eventData, "tool_use") { - b.logger.Info(ctx, "Tool use event", slog.F("event", eventData)) - } + events = append(events, event) if err := message.Accumulate(event); err != nil { b.logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) @@ -479,11 +484,94 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { return } - if err := es.TrySend(streamCtx, event); err != nil { - b.logger.Error(ctx, "failed to send event", slog.Error(err)) + // [zero] {"type":"content_block_stop"} + //[zero] {"content_block":{"id":"toolu_015YCpDbjWuSbcKGfDRWR1bD","name":"get_coordinates","type":"tool_use"},"index":1,"type":"content_block_start"} + //[zero] {"delta":{"type":"input_json_delta"},"index":1,"type":"content_block_delta"} + + switch e := event.AsAny().(type) { + case anthropic.BetaRawContentBlockStartEvent: + switch e.ContentBlock.AsAny().(type) { + case anthropic.BetaToolUseBlock: + foundToolCall = true // Ensure no more events get sent after this point since our injected tool needs to be called. + // TODO: ensure ONLY our injected tools cause this. + } + } + + if !foundToolCall { + if err := es.TrySend(streamCtx, event); err != nil { + b.logger.Error(ctx, "failed to send event", slog.Error(err)) + } + } else { + fmt.Printf("[ignored, tool call found] %s\n", event.RawJSON()) + } + } + + if foundToolCall { + for _, c := range message.Content { + switch c.AsAny().(type) { + case anthropic.BetaToolUseBlock: + fn := c.AsToolUse().Name + //input := c.AsToolUse().Input + + var input GetCoordinatesInput + raw := c.Input + err = json.Unmarshal(raw, &input) + if err != nil { + b.logger.Error(ctx, "failed to send event", slog.Error(err)) + goto outer + } + + fmt.Printf("[tool] %s %+v\n", fn, input) + resp := GetCoordinates(input.Location) + out := fmt.Sprintf("The latitude is %.2f and longitude is %.2f.", resp.Lat, resp.Long) + + _, err = coderdClient.TrackToolUse(streamCtx, &proto.TrackToolUseRequest{ + Model: string(message.Model), + Input: map[string]string{ + "location": input.Location, + }, + Tool: fn, + }) + if err != nil { + b.logger.Error(ctx, "failed to track usage", slog.Error(err)) + } + + // Start content block + var textType ant_constant.Text + if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ + Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStart]()), + Index: 0, // TODO: which index to use? + ContentBlock: anthropic.BetaRawContentBlockStartEventContentBlockUnion{ + Type: string(textType.Default()), + }, + }); err != nil { + b.logger.Error(ctx, "failed to send content block start event", slog.Error(err)) + } + + // Send the tool result + if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ + Type: string(ant_constant.ValueOf[ant_constant.ContentBlockDelta]()), + Index: 0, // TODO: which index to use? + Delta: anthropic.BetaRawMessageStreamEventUnionDelta{ + Type: string(ant_constant.ValueOf[ant_constant.TextDelta]()), + Text: out, + }, + }); err != nil { + b.logger.Error(ctx, "failed to send content block delta event", slog.Error(err)) + } + + // Stop content block + if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ + Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStop]()), + Index: 0, // TODO: which index to use? + }); err != nil { + b.logger.Error(ctx, "failed to send content block stop event", slog.Error(err)) + } + } } } +outer: // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ MsgId: message.ID, @@ -528,6 +616,38 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } +type GetCoordinatesInput struct { + Location string `json:"location" jsonschema_description:"The location to look up."` +} + +var GetCoordinatesInputSchema = GenerateSchema[GetCoordinatesInput]() + +type GetCoordinateResponse struct { + Long float64 `json:"long"` + Lat float64 `json:"lat"` +} + +func GetCoordinates(location string) GetCoordinateResponse { + return GetCoordinateResponse{ + Long: -122.4194, + Lat: 37.7749, + } +} + +func GenerateSchema[T any]() anthropic.BetaToolInputSchemaParam { + reflector := jsonschema.Reflector{ + AllowAdditionalProperties: false, + DoNotReference: true, + } + var v T + + schema := reflector.Reflect(v) + + return anthropic.BetaToolInputSchemaParam{ + Properties: schema.Properties, + } +} + func (b *Bridge) proxyAnthropicRequestPrev(w http.ResponseWriter, r *http.Request) { coderdClient, ok := b.clientFn() if !ok { diff --git a/aibridged/middleware.go b/aibridged/middleware.go index 077b8f1644c55..02896777ad1d9 100644 --- a/aibridged/middleware.go +++ b/aibridged/middleware.go @@ -40,7 +40,7 @@ func AuthMiddleware(db database.Store) func(http.Handler) http.Handler { } // extractAuthTokenForBridge extracts authorization token from HTTP request using multiple sources. -// It checks Authorization header (Bearer token), Coder session headers, and cookies. +// It checks Authorization header (Bearer token), X-Api-Key header, and Coder session headers and cookies. func extractAuthTokenForBridge(r *http.Request) string { // 1. Check Authorization header for Bearer token authHeader := r.Header.Get("Authorization") @@ -54,6 +54,12 @@ func extractAuthTokenForBridge(r *http.Request) string { } } - // 2. Fall back to Coder's standard token extraction + // 2. Check X-Api-Key header + apiKeyHeader := r.Header.Get("X-Api-Key") + if apiKeyHeader != "" { + return apiKeyHeader + } + + // 3. Fall back to Coder's standard token extraction return httpmw.APITokenFromRequest(r) } diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index b500053e981d3..e253dc8e854b7 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -323,6 +323,107 @@ func (*TrackUserPromptsResponse) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} } +type TrackToolUseRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tool string `protobuf:"bytes,1,opt,name=tool,proto3" json:"tool,omitempty"` + Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` + Input map[string]string `protobuf:"bytes,3,rep,name=input,proto3" json:"input,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *TrackToolUseRequest) Reset() { + *x = TrackToolUseRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TrackToolUseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrackToolUseRequest) ProtoMessage() {} + +func (x *TrackToolUseRequest) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrackToolUseRequest.ProtoReflect.Descriptor instead. +func (*TrackToolUseRequest) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{6} +} + +func (x *TrackToolUseRequest) GetTool() string { + if x != nil { + return x.Tool + } + return "" +} + +func (x *TrackToolUseRequest) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + +func (x *TrackToolUseRequest) GetInput() map[string]string { + if x != nil { + return x.Input + } + return nil +} + +type TrackToolUseResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TrackToolUseResponse) Reset() { + *x = TrackToolUseResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TrackToolUseResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrackToolUseResponse) ProtoMessage() {} + +func (x *TrackToolUseResponse) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrackToolUseResponse.ProtoReflect.Descriptor instead. +func (*TrackToolUseResponse) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{7} +} + var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ @@ -359,28 +460,46 @@ var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x1a, 0x0a, 0x18, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, - 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x95, 0x02, 0x0a, 0x0e, - 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4c, - 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1d, 0x2e, - 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, - 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, - 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, - 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, - 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x61, 0x69, 0x62, - 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, - 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, - 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, - 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, - 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xba, 0x01, 0x0a, 0x13, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x3f, 0x0a, + 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, + 0x6f, 0x6c, 0x55, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6e, 0x70, + 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x1a, 0x38, + 0x0a, 0x0a, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x16, 0x0a, 0x14, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x32, 0xe6, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, + 0x70, 0x74, 0x12, 0x1d, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, + 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, + 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, + 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, + 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x12, + 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, + 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, + 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -395,7 +514,7 @@ func file_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { return file_aibridged_proto_aibridged_proto_rawDescData } -var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*AuditPromptRequest)(nil), // 0: aibridged.AuditPromptRequest (*AuditPromptResponse)(nil), // 1: aibridged.AuditPromptResponse @@ -403,21 +522,27 @@ var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*TrackTokenUsageResponse)(nil), // 3: aibridged.TrackTokenUsageResponse (*TrackUserPromptsRequest)(nil), // 4: aibridged.TrackUserPromptsRequest (*TrackUserPromptsResponse)(nil), // 5: aibridged.TrackUserPromptsResponse - nil, // 6: aibridged.TrackTokenUsageRequest.OtherEntry + (*TrackToolUseRequest)(nil), // 6: aibridged.TrackToolUseRequest + (*TrackToolUseResponse)(nil), // 7: aibridged.TrackToolUseResponse + nil, // 8: aibridged.TrackTokenUsageRequest.OtherEntry + nil, // 9: aibridged.TrackToolUseRequest.InputEntry } var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ - 6, // 0: aibridged.TrackTokenUsageRequest.other:type_name -> aibridged.TrackTokenUsageRequest.OtherEntry - 0, // 1: aibridged.AIBridgeDaemon.AuditPrompt:input_type -> aibridged.AuditPromptRequest - 2, // 2: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest - 4, // 3: aibridged.AIBridgeDaemon.TrackUserPrompts:input_type -> aibridged.TrackUserPromptsRequest - 1, // 4: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse - 3, // 5: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse - 5, // 6: aibridged.AIBridgeDaemon.TrackUserPrompts:output_type -> aibridged.TrackUserPromptsResponse - 4, // [4:7] is the sub-list for method output_type - 1, // [1:4] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 8, // 0: aibridged.TrackTokenUsageRequest.other:type_name -> aibridged.TrackTokenUsageRequest.OtherEntry + 9, // 1: aibridged.TrackToolUseRequest.input:type_name -> aibridged.TrackToolUseRequest.InputEntry + 0, // 2: aibridged.AIBridgeDaemon.AuditPrompt:input_type -> aibridged.AuditPromptRequest + 2, // 3: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest + 4, // 4: aibridged.AIBridgeDaemon.TrackUserPrompts:input_type -> aibridged.TrackUserPromptsRequest + 6, // 5: aibridged.AIBridgeDaemon.TrackToolUse:input_type -> aibridged.TrackToolUseRequest + 1, // 6: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse + 3, // 7: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse + 5, // 8: aibridged.AIBridgeDaemon.TrackUserPrompts:output_type -> aibridged.TrackUserPromptsResponse + 7, // 9: aibridged.AIBridgeDaemon.TrackToolUse:output_type -> aibridged.TrackToolUseResponse + 6, // [6:10] is the sub-list for method output_type + 2, // [2:6] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_aibridged_proto_aibridged_proto_init() } @@ -498,6 +623,30 @@ func file_aibridged_proto_aibridged_proto_init() { return nil } } + file_aibridged_proto_aibridged_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TrackToolUseRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_aibridged_proto_aibridged_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TrackToolUseResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -505,7 +654,7 @@ func file_aibridged_proto_aibridged_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_aibridged_proto_aibridged_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index a37e29d534f78..b70a1d0dba52e 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -25,6 +25,13 @@ message TrackUserPromptsRequest { } message TrackUserPromptsResponse {} +message TrackToolUseRequest { + string tool = 1; + string model = 2; + map input = 3; +} +message TrackToolUseResponse {} + // AIBridgeDaemon describes the service exposed by coderd, which allows the AI bridge to send data to coderd. service AIBridgeDaemon { @@ -33,4 +40,5 @@ service AIBridgeDaemon { rpc AuditPrompt(AuditPromptRequest) returns (AuditPromptResponse); rpc TrackTokenUsage(TrackTokenUsageRequest) returns (TrackTokenUsageResponse); rpc TrackUserPrompts(TrackUserPromptsRequest) returns (TrackUserPromptsResponse); + rpc TrackToolUse(TrackToolUseRequest) returns (TrackToolUseResponse); } diff --git a/aibridged/proto/aibridged_drpc.pb.go b/aibridged/proto/aibridged_drpc.pb.go index d53aa983f2456..b2421c013769a 100644 --- a/aibridged/proto/aibridged_drpc.pb.go +++ b/aibridged/proto/aibridged_drpc.pb.go @@ -41,6 +41,7 @@ type DRPCAIBridgeDaemonClient interface { AuditPrompt(ctx context.Context, in *AuditPromptRequest) (*AuditPromptResponse, error) TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) TrackUserPrompts(ctx context.Context, in *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) + TrackToolUse(ctx context.Context, in *TrackToolUseRequest) (*TrackToolUseResponse, error) } type drpcAIBridgeDaemonClient struct { @@ -80,10 +81,20 @@ func (c *drpcAIBridgeDaemonClient) TrackUserPrompts(ctx context.Context, in *Tra return out, nil } +func (c *drpcAIBridgeDaemonClient) TrackToolUse(ctx context.Context, in *TrackToolUseRequest) (*TrackToolUseResponse, error) { + out := new(TrackToolUseResponse) + err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackToolUse", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + type DRPCAIBridgeDaemonServer interface { AuditPrompt(context.Context, *AuditPromptRequest) (*AuditPromptResponse, error) TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) TrackUserPrompts(context.Context, *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) + TrackToolUse(context.Context, *TrackToolUseRequest) (*TrackToolUseResponse, error) } type DRPCAIBridgeDaemonUnimplementedServer struct{} @@ -100,9 +111,13 @@ func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackUserPrompts(context.Context return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackToolUse(context.Context, *TrackToolUseRequest) (*TrackToolUseResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + type DRPCAIBridgeDaemonDescription struct{} -func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 3 } +func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 4 } func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -133,6 +148,15 @@ func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc. in1.(*TrackUserPromptsRequest), ) }, DRPCAIBridgeDaemonServer.TrackUserPrompts, true + case 3: + return "/aibridged.AIBridgeDaemon/TrackToolUse", drpcEncoding_File_aibridged_proto_aibridged_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAIBridgeDaemonServer). + TrackToolUse( + ctx, + in1.(*TrackToolUseRequest), + ) + }, DRPCAIBridgeDaemonServer.TrackToolUse, true default: return "", nil, nil, nil, false } @@ -189,3 +213,19 @@ func (x *drpcAIBridgeDaemon_TrackUserPromptsStream) SendAndClose(m *TrackUserPro } return x.CloseSend() } + +type DRPCAIBridgeDaemon_TrackToolUseStream interface { + drpc.Stream + SendAndClose(*TrackToolUseResponse) error +} + +type drpcAIBridgeDaemon_TrackToolUseStream struct { + drpc.Stream +} + +func (x *drpcAIBridgeDaemon_TrackToolUseStream) SendAndClose(m *TrackToolUseResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { + return err + } + return x.CloseSend() +} diff --git a/aibridged/streaming.go b/aibridged/streaming.go index 6d2397683ccf6..7f8f48d7bcdbf 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "os" "sync" @@ -132,6 +133,9 @@ func (s *eventStream) TrySend(ctx context.Context, data any, exclusions ...strin // out all the zero value objects in the response, with optional exclusions. payload, err = util.MarshalNoZero(data, exclusions...) default: + zero, _ := util.MarshalNoZero(data, exclusions...) + fmt.Printf("[zero] %s\n", zero) + payload, err = json.Marshal(data) } diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index 5de272f5151d7..b9b3709c5abac 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -50,6 +50,20 @@ func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPrompt return &proto.TrackUserPromptsResponse{}, nil } +func (s *Server) TrackToolUse(ctx context.Context, in *proto.TrackToolUseRequest) (*proto.TrackToolUseResponse, error) { + raw, err := json.Marshal(in) + if err != nil { + return nil, xerrors.Errorf("marshal event: %w", err) + } + + err = s.store.InsertWormholeEvent(ctx, database.InsertWormholeEventParams{Event: raw, EventType: "tool_use"}) + if err != nil { + return nil, xerrors.Errorf("store event: %w", err) + } + + return &proto.TrackToolUseResponse{}, nil +} + func NewServer(lifecycleCtx context.Context, store database.Store) (*Server, error) { return &Server{ lifecycleCtx: lifecycleCtx, diff --git a/coderd/database/migrations/000345_aibridge.up.sql b/coderd/database/migrations/000347_aibridge.up.sql similarity index 100% rename from coderd/database/migrations/000345_aibridge.up.sql rename to coderd/database/migrations/000347_aibridge.up.sql diff --git a/go.mod b/go.mod index 411a996d7aacb..d73d1c937209f 100644 --- a/go.mod +++ b/go.mod @@ -349,7 +349,7 @@ require ( github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect - github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect + github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect github.com/jsimonetti/rtnetlink v1.3.5 // indirect @@ -486,6 +486,7 @@ require ( github.com/coder/aisdk-go v0.0.9 github.com/coder/preview v1.0.2 github.com/fsnotify/fsnotify v1.9.0 + github.com/invopop/jsonschema v0.13.0 github.com/mark3labs/mcp-go v0.32.0 github.com/openai/openai-go v1.8.1 ) @@ -507,7 +508,9 @@ require ( github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aquasecurity/trivy v0.58.2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect @@ -530,6 +533,7 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/tmaxmax/go-sse v0.11.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect diff --git a/go.sum b/go.sum index 1286a0912e4fe..dc2191f14f1ab 100644 --- a/go.sum +++ b/go.sum @@ -790,6 +790,8 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= @@ -828,6 +830,8 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM= github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g= @@ -1413,6 +1417,8 @@ github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwso github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o= github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -1849,6 +1855,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= From 0581ef2d0cf086a28807f774a056b2b5a87dad73 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 4 Jul 2025 16:47:34 +0200 Subject: [PATCH 25/61] WIP: MCP integration Signed-off-by: Danny Kopping --- aibridged/bridge.go | 94 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 04d024ac04479..04da570b9e91a 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -16,7 +16,10 @@ import ( "sync" "time" + "cdr.dev/slog" "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/anthropics/anthropic-sdk-go/packages/param" ant_ssestream "github.com/anthropics/anthropic-sdk-go/packages/ssestream" ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" "github.com/google/uuid" @@ -24,8 +27,6 @@ import ( "github.com/openai/openai-go/shared/constant" "golang.org/x/xerrors" - "cdr.dev/slog" - "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/codersdk" @@ -401,6 +402,26 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } in.Tools = tools + // TODO: fetch instead with lib and inject into tools since OAI doesn't support MCP. + in.MCPServers = []anthropic.BetaRequestMCPServerURLDefinitionParam{ + { + URL: "https://api.githubcopilot.com/mcp/", + Name: "github", + AuthorizationToken: param.NewOpt(os.Getenv("GITHUB_MCP_TOKEN")), + ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{ + Enabled: anthropic.Bool(true), + }, + }, + { + URL: "https://dev.coder.com/api/experimental/mcp/http", + Name: "coder", + AuthorizationToken: param.NewOpt(os.Getenv("CODER_MCP_TOKEN")), + ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{ + Enabled: anthropic.Bool(true), + }, + }, + } + // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). isSmallFastModel := strings.Contains(string(in.Model), "3-5-haiku") @@ -418,7 +439,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") - client := anthropic.NewClient() + client := anthropic.NewClient(option.WithHeader("anthropic-beta", anthropic.AnthropicBetaMCPClient2025_04_04)) if !in.UseStreaming() { msg, err := client.Beta.Messages.New(ctx, in.BetaMessageNewParams) if err != nil { @@ -478,32 +499,36 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { event := stream.Current() events = append(events, event) + fmt.Printf("[provider] %s\n", event.RawJSON()) + if err := message.Accumulate(event); err != nil { b.logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) http.Error(w, "failed to proxy request", http.StatusInternalServerError) return } + // Regular tool call: // [zero] {"type":"content_block_stop"} //[zero] {"content_block":{"id":"toolu_015YCpDbjWuSbcKGfDRWR1bD","name":"get_coordinates","type":"tool_use"},"index":1,"type":"content_block_start"} //[zero] {"delta":{"type":"input_json_delta"},"index":1,"type":"content_block_delta"} - switch e := event.AsAny().(type) { - case anthropic.BetaRawContentBlockStartEvent: - switch e.ContentBlock.AsAny().(type) { - case anthropic.BetaToolUseBlock: - foundToolCall = true // Ensure no more events get sent after this point since our injected tool needs to be called. - // TODO: ensure ONLY our injected tools cause this. - } - } - - if !foundToolCall { - if err := es.TrySend(streamCtx, event); err != nil { - b.logger.Error(ctx, "failed to send event", slog.Error(err)) - } - } else { - fmt.Printf("[ignored, tool call found] %s\n", event.RawJSON()) + //switch e := event.AsAny().(type) { + //case anthropic.BetaRawContentBlockStartEvent: + // switch block := e.ContentBlock.AsAny().(type) { + // case anthropic.BetaToolUseBlock: + // if block.Name == "get_coordinates" { + // foundToolCall = true // Ensure no more events get sent after this point since our injected tool needs to be called. + // } + // } + //} + // + //if !foundToolCall { + if err := es.TrySend(streamCtx, event); err != nil { + b.logger.Error(ctx, "failed to send event", slog.Error(err)) } + //} else { + // fmt.Printf("[ignored, tool call found] %s\n", event.RawJSON()) + //} } if foundToolCall { @@ -572,8 +597,34 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } outer: + for _, c := range message.Content { + switch block := c.AsAny().(type) { + case anthropic.BetaMCPToolUseBlock: + + var input map[string]string + if err := json.Unmarshal([]byte(block.JSON.Input.Raw()), &input); err != nil { + b.logger.Error(ctx, "failed to marshal tool input", slog.Error(err)) + continue + } + + if _, err = coderdClient.TrackToolUse(streamCtx, &proto.TrackToolUseRequest{ + Tool: block.Name, + Model: string(message.Model), + Input: input, + }); err != nil { + b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) + } + case anthropic.BetaMCPToolResultBlock: + for _, res := range block.Content.AsBetaMCPToolResultBlockContent() { + // TODO: store results? + x := res.Text + fmt.Println(x) + } + } + } + // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! - _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ + if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ MsgId: message.ID, Model: string(message.Model), InputTokens: message.Usage.InputTokens, @@ -585,9 +636,8 @@ outer: "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, }, - }) - if err != nil { - b.logger.Error(ctx, "failed to track usage", slog.Error(err)) + }); err != nil { + b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) } var streamErr error From 7a9f0ac1393ec16ee51823052364498a96d03953 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 7 Jul 2025 16:12:56 +0200 Subject: [PATCH 26/61] chore: handle errors better Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 4 + aibridged/bridge.go | 317 ++++++++++++++++++++++------------------- aibridged/constants.go | 5 + aibridged/streaming.go | 30 +++- coderd/aibridge.go | 22 ++- 5 files changed, 233 insertions(+), 145 deletions(-) create mode 100644 aibridged/constants.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 2d4ed97257217..462eac8bcc8fe 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -255,6 +255,10 @@ func (s *Server) BridgeAddr() string { return s.bridge.Addr() } +func (s *Server) BridgeErr() error { + return s.bridge.lastErr +} + // TODO: direct copy/paste from provisionerd, abstract into common util. func retryable(err error) bool { return xerrors.Is(err, yamux.ErrSessionShutdown) || xerrors.Is(err, io.EOF) || xerrors.Is(err, fasthttputil.ErrInmemoryListenerClosed) || diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 04da570b9e91a..78e06c0cdc993 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -1,15 +1,13 @@ package aibridged import ( - "bytes" - "compress/gzip" "context" "encoding/json" + "errors" "fmt" "io" "net" "net/http" - "net/http/httputil" "net/url" "os" "strings" @@ -20,7 +18,6 @@ import ( "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" "github.com/anthropics/anthropic-sdk-go/packages/param" - ant_ssestream "github.com/anthropics/anthropic-sdk-go/packages/ssestream" ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" "github.com/google/uuid" "github.com/openai/openai-go" @@ -33,6 +30,33 @@ import ( "github.com/invopop/jsonschema" ) +// Error type constants for structured error reporting +const ( + ErrorTypeRequestCanceled = "request_canceled" + ErrorTypeConnectionError = "connection_error" + ErrorTypeUnexpectedError = "unexpected_error" + ErrorTypeAnthropicAPIError = "anthropic_api_error" + ErrorTypeOpenAIAPIError = "openai_api_error" + ErrorTypeInternalError = "internal_error" + ErrorTypeValidationError = "validation_error" + ErrorTypeAuthenticationError = "authentication_error" + ErrorTypeRateLimitError = "rate_limit_error" + ErrorTypeTimeoutError = "timeout_error" +) + +// BridgeError represents a structured error from the bridge that can carry +// specific error information back to the client. +type BridgeError struct { + Code string `json:"code"` + Message string `json:"message"` + StatusCode int `json:"status_code"` + Details map[string]string `json:"details,omitempty"` +} + +func (e *BridgeError) Error() string { + return e.Message +} + type Bridge struct { cfg codersdk.AIBridgeConfig @@ -40,6 +64,8 @@ type Bridge struct { addr string clientFn func() (proto.DRPCAIBridgeDaemonClient, bool) logger slog.Logger + + lastErr error } func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) *Bridge { @@ -68,15 +94,20 @@ func (b *Bridge) openAITarget() *url.URL { target, err := url.Parse(u) if err != nil { panic(fmt.Sprintf("failed to parse %q", u)) - } return target } func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { sessionID := uuid.New() + b.logger.Info(r.Context(), "OpenAI request started", slog.F("sessionID", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) + + // Clear any previous error state + b.clearError() + defer func() { + b.logger.Info(r.Context(), "OpenAI request ended", slog.F("sessionID", sessionID)) _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) }() @@ -104,6 +135,10 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { + if isConnectionError(err) { + b.logger.Debug(r.Context(), "client disconnected during request body read", slog.Error(err)) + return // Don't send error response if client already disconnected + } b.logger.Error(r.Context(), "failed to read body", slog.Error(err)) http.Error(w, "failed to read body", http.StatusInternalServerError) return @@ -267,7 +302,10 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } if err := es.TrySend(streamCtx, toolChunk); err != nil { - b.logger.Error(ctx, "failed to send tool chunk", slog.Error(err)) + b.logConnectionError(ctx, err, "sending tool chunk") + if isConnectionError(err) { + return // Stop processing if client disconnected + } } finishChunk := openai.ChatCompletionChunk{ @@ -290,7 +328,10 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } if err := es.TrySend(streamCtx, finishChunk, "choices[].delta.content"); err != nil { - b.logger.Error(ctx, "failed to send finish chunk", slog.Error(err)) + b.logConnectionError(ctx, err, "sending finish chunk") + if isConnectionError(err) { + return // Stop processing if client disconnected + } } } continue @@ -302,7 +343,10 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // is appended as if it came from the assistant. if _, ok := ignoreSubsequent[acc.ID]; !ok { if err := es.TrySend(streamCtx, chunk); err != nil { - b.logger.Error(ctx, "failed to send reflected chunk", slog.Error(err)) + b.logConnectionError(ctx, err, "sending reflected chunk") + if isConnectionError(err) { + return // Stop processing if client disconnected + } } } } @@ -312,8 +356,14 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } if err := stream.Err(); err != nil { - // TODO: handle error. - b.logger.Error(ctx, "server stream error", slog.Error(err)) + if isConnectionError(err) { + b.logger.Debug(ctx, "upstream connection closed", slog.Error(err)) + } else { + b.logger.Error(ctx, "server stream error", slog.Error(err)) + b.setError(err) + } + + http.Error(w, err.Error(), http.StatusInternalServerError) } wg.Wait() @@ -330,6 +380,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { completion, err := client.Chat.Completions.New(ctx, in.ChatCompletionNewParams) if err != nil { b.logger.Error(ctx, "chat completion failed", slog.Error(err)) + b.setError(err) http.Error(w, "chat completion failed", http.StatusInternalServerError) return } @@ -341,8 +392,14 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { sessionID := uuid.New() + b.logger.Info(r.Context(), "Anthropic request started", slog.F("sessionID", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) + + // Clear any previous error state + b.clearError() + defer func() { + b.logger.Info(r.Context(), "Anthropic request ended", slog.F("sessionID", sessionID)) _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) }() @@ -443,7 +500,14 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if !in.UseStreaming() { msg, err := client.Beta.Messages.New(ctx, in.BetaMessageNewParams) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + if antErr := getAnthropicErrorResponse(err); antErr != nil { + b.setError(antErr) + http.Error(w, antErr.Error.Message, antErr.StatusCode) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + b.setError(err) + } + return } // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! @@ -524,7 +588,10 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // //if !foundToolCall { if err := es.TrySend(streamCtx, event); err != nil { - b.logger.Error(ctx, "failed to send event", slog.Error(err)) + b.logConnectionError(ctx, err, "sending event") + if isConnectionError(err) { + return // Stop processing if client disconnected + } } //} else { // fmt.Printf("[ignored, tool call found] %s\n", event.RawJSON()) @@ -642,7 +709,22 @@ outer: var streamErr error if streamErr = stream.Err(); streamErr != nil { - http.Error(w, stream.Err().Error(), http.StatusInternalServerError) + if isConnectionError(streamErr) { + b.logger.Warn(ctx, "upstream connection closed", slog.Error(streamErr)) + } + + b.logger.Error(ctx, "anthropic stream error", slog.Error(streamErr)) + b.setError(streamErr) + if antErr := getAnthropicErrorResponse(streamErr); antErr != nil { + err = es.TrySend(streamCtx, antErr) + if err != nil { + b.logger.Error(ctx, "failed to send error", slog.Error(err)) + } + + http.Error(w, antErr.Error.Message, ProxyErrCode) + } else { + http.Error(w, streamErr.Error(), ProxyErrCode) + } } err = es.Close(streamCtx) @@ -666,6 +748,40 @@ outer: } } +func getAnthropicErrorResponse(err error) *AnthropicErrorResponse { + var apierr *anthropic.Error + if !errors.As(err, &apierr) { + return nil + } + + msg := apierr.Error() + + var detail *anthropic.BetaAPIError + if field, ok := apierr.JSON.ExtraFields["error"]; ok { + _ = json.Unmarshal([]byte(field.Raw()), &detail) + } + if detail != nil { + msg = detail.Message + } + + return &AnthropicErrorResponse{ + BetaErrorResponse: &anthropic.BetaErrorResponse{ + Error: anthropic.BetaErrorUnion{ + Message: msg, + Type: string(detail.Type), + }, + Type: ant_constant.ValueOf[ant_constant.Error](), + }, + StatusCode: apierr.StatusCode, + } +} + +type AnthropicErrorResponse struct { + *anthropic.BetaErrorResponse + + StatusCode int `json:"-"` +} + type GetCoordinatesInput struct { Location string `json:"location" jsonschema_description:"The location to look up."` } @@ -698,150 +814,65 @@ func GenerateSchema[T any]() anthropic.BetaToolInputSchemaParam { } } -func (b *Bridge) proxyAnthropicRequestPrev(w http.ResponseWriter, r *http.Request) { - coderdClient, ok := b.clientFn() - if !ok { - // TODO: log issue. - http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) - return - } - - target, err := url.Parse("https://api.anthropic.com") +func (b *Bridge) Serve() error { + list, err := net.Listen("tcp", b.httpSrv.Addr) if err != nil { - http.Error(w, "failed to parse Anthropic URL", http.StatusInternalServerError) - return + return xerrors.Errorf("listen: %w", err) } - proxy := httputil.NewSingleHostReverseProxy(target) - originalDirector := proxy.Director - proxy.Director = func(req *http.Request) { - originalDirector(req) - - // Add Anthropic-specific headers - if strings.TrimSpace(req.Header.Get("x-api-key")) == "" { - req.Header.Set("x-api-key", os.Getenv("ANTHROPIC_API_KEY")) - } - req.Header.Set("anthropic-version", "2023-06-01") - - if req.Header.Get("Content-Type") == "" { - req.Header.Set("Content-Type", "application/json") - } - - req.Host = target.Host - req.URL.Scheme = target.Scheme - req.URL.Host = target.Host + b.addr = list.Addr().String() - body, err := io.ReadAll(req.Body) - if err != nil { - http.Error(w, "could not ready request body", http.StatusBadRequest) - return - } - _ = req.Body.Close() + return b.httpSrv.Serve(list) // TODO: TLS. +} - var msg anthropic.MessageNewParams - err = json.NewDecoder(bytes.NewReader(body)).Decode(&msg) - if err != nil { - http.Error(w, "could not unmarshal request body", http.StatusBadRequest) - return - } +func (b *Bridge) Addr() string { + return b.addr +} - // TODO: robustness - if len(msg.Messages) > 0 { - latest := msg.Messages[len(msg.Messages)-1] - if len(latest.Content) > 0 { - if latest.Content[0].OfText != nil { - _, _ = coderdClient.TrackUserPrompts(r.Context(), &proto.TrackUserPromptsRequest{ - Prompt: latest.Content[0].OfText.Text, - }) - } else { - fmt.Println() - } +// setError sets a structured error with appropriate context +func (b *Bridge) setError(val any) { + switch err := val.(type) { + case error: + switch { + case errors.Is(err, context.Canceled): + b.lastErr = &BridgeError{ + Code: ErrorTypeRequestCanceled, + Message: "Request was canceled", + StatusCode: http.StatusRequestTimeout, } - } - - req.Body = io.NopCloser(bytes.NewReader(body)) - - fmt.Printf("Proxying %s request to: %s\n", req.Method, req.URL.String()) - } - proxy.ModifyResponse = func(response *http.Response) error { - body, err := io.ReadAll(response.Body) - if err != nil { - return xerrors.Errorf("read response body: %w", err) - } - if err = response.Body.Close(); err != nil { - return xerrors.Errorf("close body: %w", err) - } - - if !strings.Contains(response.Header.Get("Content-Type"), "text/event-stream") { - var msg anthropic.Message - - // TODO: check content-encoding to handle others. - gr, err := gzip.NewReader(bytes.NewReader(body)) - if err != nil { - return xerrors.Errorf("parse gzip-encoded body: %w", err) + case isConnectionError(err): + b.lastErr = &BridgeError{ + Code: ErrorTypeConnectionError, + Message: "Connection to upstream service failed", + StatusCode: http.StatusBadGateway, } - - err = json.NewDecoder(gr).Decode(&msg) - if err != nil { - return xerrors.Errorf("parse non-streaming body: %w", err) + default: + b.lastErr = &BridgeError{ + Code: ErrorTypeUnexpectedError, + Message: err.Error(), + StatusCode: http.StatusInternalServerError, } - - _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ - MsgId: msg.ID, - InputTokens: msg.Usage.InputTokens, - OutputTokens: msg.Usage.OutputTokens, - }) - - response.Body = io.NopCloser(bytes.NewReader(body)) - return nil } - - response.Body = io.NopCloser(bytes.NewReader(body)) - stream := ant_ssestream.NewStream[anthropic.MessageStreamEventUnion](ant_ssestream.NewDecoder(response), nil) - - var ( - inputToks, outputToks int64 - ) - - var msg anthropic.Message - for stream.Next() { - event := stream.Current() - err = msg.Accumulate(event) - if err != nil { - // TODO: don't panic. - panic(err) - } - - if msg.Usage.InputTokens+msg.Usage.OutputTokens > 0 { - inputToks = msg.Usage.InputTokens - outputToks = msg.Usage.OutputTokens - } + case AnthropicErrorResponse: + b.lastErr = &BridgeError{ + Code: ErrorTypeAnthropicAPIError, + Message: err.Error.Message, + Details: map[string]string{"type": err.Error.Type}, + StatusCode: err.StatusCode, } - - _, _ = coderdClient.TrackTokenUsage(r.Context(), &proto.TrackTokenUsageRequest{ - MsgId: msg.ID, - InputTokens: inputToks, - OutputTokens: outputToks, - }) - - response.Body = io.NopCloser(bytes.NewReader(body)) - - return nil } - proxy.ServeHTTP(w, r) } -func (b *Bridge) Serve() error { - list, err := net.Listen("tcp", b.httpSrv.Addr) - if err != nil { - return xerrors.Errorf("listen: %w", err) - } - - b.addr = list.Addr().String() - - return b.httpSrv.Serve(list) // TODO: TLS. +// clearError clears the error state when a new request starts +func (b *Bridge) clearError() { + b.lastErr = nil } -func (b *Bridge) Addr() string { - return b.addr +// logConnectionError logs connection errors with appropriate severity +func (b *Bridge) logConnectionError(ctx context.Context, err error, operation string) { + if isConnectionError(err) { + b.logger.Debug(ctx, "client disconnected during "+operation, slog.Error(err)) + } else { + b.logger.Error(ctx, "error during "+operation, slog.Error(err)) + } } diff --git a/aibridged/constants.go b/aibridged/constants.go new file mode 100644 index 0000000000000..c274f62ed120b --- /dev/null +++ b/aibridged/constants.go @@ -0,0 +1,5 @@ +package aibridged + +const ( + ProxyErrCode = 1500 +) diff --git a/aibridged/streaming.go b/aibridged/streaming.go index 7f8f48d7bcdbf..b104c9bfcb02a 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -4,10 +4,14 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" + "io" "net/http" "os" + "strings" "sync" + "syscall" "github.com/google/uuid" "golang.org/x/xerrors" @@ -17,6 +21,26 @@ import ( "github.com/coder/coder/v2/aibridged/util" ) +// isConnectionError checks if an error is related to client disconnection +func isConnectionError(err error) bool { + if err == nil { + return false + } + + if errors.Is(err, io.EOF) { + return true + } + + if errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.EPIPE) { + return true + } + + errStr := err.Error() + return strings.Contains(errStr, "broken pipe") || + strings.Contains(errStr, "connection reset by peer") || + strings.Contains(errStr, "use of closed network connection") +} + // BasicSSESender was implemented to overcome httpapi.ServerSentEventSender's odd design choices. For example, it doesn't // write "event: data" for every data event (it's unnecessary, and breaks some AI tools' parsing of the SSE stream). func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, stream EventStreamer, logger slog.Logger) http.HandlerFunc { @@ -50,7 +74,11 @@ func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, stream EventS _, err := w.Write(payload) if err != nil { - logger.Error(ctx, "failed to write SSE event", slog.Error(err)) + if isConnectionError(err) { + logger.Debug(ctx, "client disconnected during SSE write", slog.Error(err)) + } else { + logger.Error(ctx, "failed to write SSE event", slog.Error(err)) + } return } flush(w) diff --git a/coderd/aibridge.go b/coderd/aibridge.go index 0f4aeadd8e804..5de75eacc77f4 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -8,9 +8,29 @@ import ( "cdr.dev/slog" + "github.com/coder/coder/v2/aibridged" "github.com/coder/coder/v2/coderd/util/slice" ) +type rt struct { + http.RoundTripper + + server *aibridged.Server +} + +func (r *rt) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := r.RoundTripper.RoundTrip(req) + + if err != nil || resp.StatusCode == aibridged.ProxyErrCode { + lastErr := r.server.BridgeErr() + if lastErr != nil { + return resp, lastErr + } + } + + return resp, err +} + func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -39,9 +59,9 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { } rp := httputil.NewSingleHostReverseProxy(u) + rp.Transport = &rt{RoundTripper: http.DefaultTransport, server: server} rp.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { api.Logger.Error(ctx, "aibridge reverse proxy error", slog.Error(err)) - http.Error(w, "aibridge internal error", http.StatusBadGateway) } http.StripPrefix("/api/v2/aibridge", rp).ServeHTTP(rw, r) } From 481671e6c1ce8a51dbd6f89f69dcbac317e2debb Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 8 Jul 2025 17:07:29 +0200 Subject: [PATCH 27/61] WIP: MCP injection, tool calling Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 6 +- aibridged/bridge.go | 366 +++++++++++++++------- aibridged/mcp.go | 110 +++++++ aibridged/streaming.go | 43 ++- coderd/aibridgedserver/aibridgedserver.go | 2 +- 5 files changed, 388 insertions(+), 139 deletions(-) create mode 100644 aibridged/mcp.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 462eac8bcc8fe..717f1398b2449 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -71,7 +71,11 @@ func New(rpcDialer Dialer, httpAddr string, logger slog.Logger, bridgeCfg coders initConnectionCh: make(chan struct{}), } - bridge := NewBridge(bridgeCfg, httpAddr, logger.Named("ai_bridge"), daemon.client) + bridge, err := NewBridge(bridgeCfg, httpAddr, logger.Named("ai_bridge"), daemon.client) + if err != nil { + return nil, xerrors.Errorf("create new bridge server: %w", err) + } + daemon.bridge = bridge daemon.wg.Add(1) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 78e06c0cdc993..1ee2b87b0c4a2 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -17,17 +17,18 @@ import ( "cdr.dev/slog" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" - "github.com/anthropics/anthropic-sdk-go/packages/param" ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" "github.com/google/uuid" + "github.com/mark3labs/mcp-go/mcp" "github.com/openai/openai-go" "github.com/openai/openai-go/shared/constant" + "golang.org/x/sync/errgroup" "golang.org/x/xerrors" + "github.com/invopop/jsonschema" + "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/codersdk" - - "github.com/invopop/jsonschema" ) // Error type constants for structured error reporting @@ -65,10 +66,11 @@ type Bridge struct { clientFn func() (proto.DRPCAIBridgeDaemonClient, bool) logger slog.Logger - lastErr error + lastErr error + mcpProxies map[string]*BridgeMCPProxy } -func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) *Bridge { +func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) (*Bridge, error) { var bridge Bridge mux := &http.ServeMux{} @@ -86,7 +88,28 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli bridge.clientFn = clientFn bridge.logger = logger - return &bridge + const githubMCPName = "github" + githubMCP, err := NewBridgeMCPProxy(githubMCPName, "https://api.githubcopilot.com/mcp/", os.Getenv("GITHUB_MCP_TOKEN"), logger.Named("mcp-proxy")) + if err != nil { + return nil, xerrors.Errorf("github MCP proxy setup: %w", err) + } + + bridge.mcpProxies = map[string]*BridgeMCPProxy{ + githubMCPName: githubMCP, + } + + ctx := context.Background() + + var eg errgroup.Group + eg.Go(func() error { + return githubMCP.Init(ctx) + }) + + if err := eg.Wait(); err != nil { + return nil, xerrors.Errorf("MCP proxy init: %w", err) + } + + return &bridge, nil } func (b *Bridge) openAITarget() *url.URL { @@ -288,7 +311,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { Choices: []openai.ChatCompletionChunkChoice{ { Delta: openai.ChatCompletionChunkChoiceDelta{ - //Role: "assistant", + // Role: "assistant", Content: fmt.Sprintf(" %s", toolRes.Choices[0].Message.Content), // TODO: improve }, }, @@ -301,7 +324,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { Object: constant.ValueOf[constant.ChatCompletionChunk](), } - if err := es.TrySend(streamCtx, toolChunk); err != nil { + if err := es.TrySend(streamCtx, toolChunk, chunk.RawJSON()); err != nil { b.logConnectionError(ctx, err, "sending tool chunk") if isConnectionError(err) { return // Stop processing if client disconnected @@ -313,7 +336,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { Choices: []openai.ChatCompletionChunkChoice{ { Delta: openai.ChatCompletionChunkChoiceDelta{ - //Role: "assistant", + // Role: "assistant", Content: "", }, FinishReason: string(openai.CompletionChoiceFinishReasonStop), @@ -327,7 +350,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { Object: constant.ValueOf[constant.ChatCompletionChunk](), } - if err := es.TrySend(streamCtx, finishChunk, "choices[].delta.content"); err != nil { + if err := es.TrySend(streamCtx, finishChunk, chunk.RawJSON(), "choices[].delta.content"); err != nil { b.logConnectionError(ctx, err, "sending finish chunk") if isConnectionError(err) { return // Stop processing if client disconnected @@ -342,7 +365,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // up the stream need to be ignored because we send those after the tool call is executed and the result // is appended as if it came from the assistant. if _, ok := ignoreSubsequent[acc.ID]; !ok { - if err := es.TrySend(streamCtx, chunk); err != nil { + if err := es.TrySend(streamCtx, chunk, chunk.RawJSON()); err != nil { b.logConnectionError(ctx, err, "sending reflected chunk") if isConnectionError(err) { return // Stop processing if client disconnected @@ -403,8 +426,8 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) }() - //out, _ := httputil.DumpRequest(r, true) - //fmt.Printf("\n\nREQUEST: %s\n\n", out) + // out, _ := httputil.DumpRequest(r, true) + // fmt.Printf("\n\nREQUEST: %s\n\n", out) // Allow us to interrupt watch via cancel. ctx, cancel := context.WithCancel(r.Context()) @@ -422,8 +445,8 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if !useBeta { b.logger.Warn(r.Context(), "non-beta API requested, using beta instead", slog.F("url", r.URL.String())) useBeta = true - //http.Error(w, "only beta API supported", http.StatusInternalServerError) - //return + // http.Error(w, "only beta API supported", http.StatusInternalServerError) + // return } body, err := io.ReadAll(r.Body) @@ -433,8 +456,8 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { return } - //var in streamer - //if useBeta { + // var in streamer + // if useBeta { var in BetaMessageNewParamsWrapper //} else { // in = &MessageNewParamsWrapper{} @@ -446,38 +469,36 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { return } - toolParams := []anthropic.BetaToolParam{ - { - Name: "get_coordinates", - Description: anthropic.String("Accepts a place as an address, then returns the latitude and longitude coordinates."), - InputSchema: GetCoordinatesInputSchema, - }, - } - tools := make([]anthropic.BetaToolUnionParam, len(toolParams)) - for i, toolParam := range toolParams { - tools[i] = anthropic.BetaToolUnionParam{OfTool: &toolParam} - } - in.Tools = tools - - // TODO: fetch instead with lib and inject into tools since OAI doesn't support MCP. - in.MCPServers = []anthropic.BetaRequestMCPServerURLDefinitionParam{ - { - URL: "https://api.githubcopilot.com/mcp/", - Name: "github", - AuthorizationToken: param.NewOpt(os.Getenv("GITHUB_MCP_TOKEN")), - ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{ - Enabled: anthropic.Bool(true), - }, - }, - { - URL: "https://dev.coder.com/api/experimental/mcp/http", - Name: "coder", - AuthorizationToken: param.NewOpt(os.Getenv("CODER_MCP_TOKEN")), - ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{ - Enabled: anthropic.Bool(true), - }, - }, - } + in.Tools = append(in.Tools, anthropic.BetaToolUnionParam{OfTool: &anthropic.BetaToolParam{ + Name: "get_coordinates", + Description: anthropic.String("Accepts a place as an address, then returns the latitude and longitude coordinates."), + InputSchema: GetCoordinatesInputSchema, + }}) + + for _, proxy := range b.mcpProxies { + in.Tools = append(in.Tools, proxy.ListTools()...) + } + // in.Tools = append(in.Tools, b.fetchMCPTools("", "")...) + + //// TODO: fetch instead with lib and inject into tools since OAI doesn't support MCP. + //in.MCPServers = []anthropic.BetaRequestMCPServerURLDefinitionParam{ + // { + // URL: "https://api.githubcopilot.com/mcp/", + // Name: "github", + // AuthorizationToken: param.NewOpt(os.Getenv("GITHUB_MCP_TOKEN")), + // ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{ + // Enabled: anthropic.Bool(true), + // }, + // }, + // { + // URL: "https://dev.coder.com/api/experimental/mcp/http", + // Name: "coder", + // AuthorizationToken: param.NewOpt(os.Getenv("CODER_MCP_TOKEN")), + // ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{ + // Enabled: anthropic.Bool(true), + // }, + // }, + //} // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). isSmallFastModel := strings.Contains(string(in.Model), "3-5-haiku") @@ -539,6 +560,12 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { es := newEventStream(anthropicEventStream) + //var buf strings.Builder + //in.Messages[0].Content = []anthropic.BetaContentBlockParamUnion{in.Messages[0].Content[len(in.Messages[0].Content) - 1]} + // + //json.NewEncoder(&buf).Encode(in) + //fmt.Println(strings.Replace(buf.String(), "'", "\\'", -1)) + var wg sync.WaitGroup wg.Add(1) @@ -555,15 +582,13 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { stream := client.Beta.Messages.NewStreaming(streamCtx, in.BetaMessageNewParams) - var foundToolCall bool - var events []anthropic.BetaRawMessageStreamEventUnion var message anthropic.BetaMessage for stream.Next() { event := stream.Current() events = append(events, event) - fmt.Printf("[provider] %s\n", event.RawJSON()) + //fmt.Printf("[provider] %s\n", event.RawJSON()) if err := message.Accumulate(event); err != nil { b.logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) @@ -576,94 +601,193 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { //[zero] {"content_block":{"id":"toolu_015YCpDbjWuSbcKGfDRWR1bD","name":"get_coordinates","type":"tool_use"},"index":1,"type":"content_block_start"} //[zero] {"delta":{"type":"input_json_delta"},"index":1,"type":"content_block_delta"} - //switch e := event.AsAny().(type) { - //case anthropic.BetaRawContentBlockStartEvent: - // switch block := e.ContentBlock.AsAny().(type) { - // case anthropic.BetaToolUseBlock: - // if block.Name == "get_coordinates" { - // foundToolCall = true // Ensure no more events get sent after this point since our injected tool needs to be called. - // } - // } - //} - // - //if !foundToolCall { - if err := es.TrySend(streamCtx, event); err != nil { - b.logConnectionError(ctx, err, "sending event") - if isConnectionError(err) { - return // Stop processing if client disconnected - } - } - //} else { - // fmt.Printf("[ignored, tool call found] %s\n", event.RawJSON()) - //} - } + var appendedToolMessages []anthropic.BetaContentBlockParamUnion - if foundToolCall { - for _, c := range message.Content { - switch c.AsAny().(type) { + switch event.Type { + case string(constant.ValueOf[ant_constant.ContentBlockStart]()): // Have to do this because otherwise content_block_delta and content_block_start both match the type anthropic.BetaRawContentBlockStartEvent + switch block := event.AsContentBlockStart().ContentBlock.AsAny().(type) { case anthropic.BetaToolUseBlock: - fn := c.AsToolUse().Name - //input := c.AsToolUse().Input - - var input GetCoordinatesInput - raw := c.Input - err = json.Unmarshal(raw, &input) - if err != nil { - b.logger.Error(ctx, "failed to send event", slog.Error(err)) - goto outer + // if block.Name == "get_coordinates" { + // foundToolCall = true // Ensure no more events get sent after this point since our injected tool needs to be called. + // } + fmt.Printf("[event] %s\n[tool] %s %+v\n\n", event.RawJSON(), block.Name, block.Input) + + serverName, toolName, found := strings.Cut(block.Name, MCPProxyDelimiter) + if !found { + // Not an MCP proxy call, don't do anything. + continue } - fmt.Printf("[tool] %s %+v\n", fn, input) - resp := GetCoordinates(input.Location) - out := fmt.Sprintf("The latitude is %.2f and longitude is %.2f.", resp.Lat, resp.Long) - _, err = coderdClient.TrackToolUse(streamCtx, &proto.TrackToolUseRequest{ Model: string(message.Model), - Input: map[string]string{ - "location": input.Location, - }, - Tool: fn, + Input: map[string]string{}, // TODO: input. + Tool: toolName, }) if err != nil { b.logger.Error(ctx, "failed to track usage", slog.Error(err)) } - // Start content block - var textType ant_constant.Text - if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ - Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStart]()), - Index: 0, // TODO: which index to use? - ContentBlock: anthropic.BetaRawContentBlockStartEventContentBlockUnion{ - Type: string(textType.Default()), - }, - }); err != nil { - b.logger.Error(ctx, "failed to send content block start event", slog.Error(err)) + res, err := b.mcpProxies[serverName].CallTool(streamCtx, toolName, block.Input) + if err != nil { + // TODO: } - // Send the tool result - if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ - Type: string(ant_constant.ValueOf[ant_constant.ContentBlockDelta]()), - Index: 0, // TODO: which index to use? - Delta: anthropic.BetaRawMessageStreamEventUnionDelta{ - Type: string(ant_constant.ValueOf[ant_constant.TextDelta]()), - Text: out, - }, - }); err != nil { - b.logger.Error(ctx, "failed to send content block delta event", slog.Error(err)) + var out strings.Builder + if err := json.NewEncoder(&out).Encode(res); err != nil { + b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) + continue } - // Stop content block - if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ - Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStop]()), - Index: 0, // TODO: which index to use? - }); err != nil { - b.logger.Error(ctx, "failed to send content block stop event", slog.Error(err)) + //// Start content block + //var textType ant_constant.Text + //appendedToolMessages = append(appendedToolMessages, anthropic.BetaRawMessageStreamEventUnion{ + // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStart]()), + // Index: 0, // TODO: which index to use? + // ContentBlock: anthropic.BetaRawContentBlockStartEventContentBlockUnion{ + // Type: string(textType.Default()), + // }, + //}) + + for _, content := range res.Content { + switch cb := content.(type) { + case mcp.TextContent: + appendedToolMessages = append(appendedToolMessages, + anthropic.NewBetaToolResultBlock(block.ID, cb.Text, false)) + default: + // Not supported. + appendedToolMessages = append(appendedToolMessages, + anthropic.NewBetaToolResultBlock(block.ID, out.String(), false)) + } } + + //// Start content block + //var textType ant_constant.Text + //appendedToolMessages = append(appendedToolMessages, anthropic.BetaRawMessageStreamEventUnion{ + // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStart]()), + // Index: 0, // TODO: which index to use? + // ContentBlock: anthropic.BetaRawContentBlockStartEventContentBlockUnion{ + // Type: string(textType.Default()), + // }, + //}) + // + //// Send the tool result + //appendedToolMessages = append(appendedToolMessages, anthropic.BetaRawMessageStreamEventUnion{ + // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockDelta]()), + // Index: 0, // TODO: which index to use? + // Delta: anthropic.BetaRawMessageStreamEventUnionDelta{ + // Type: string(ant_constant.ValueOf[ant_constant.TextDelta]()), + // Text: out.String(), + // }, + //}) + // + //// Stop content block + //appendedToolMessages = append(appendedToolMessages, anthropic.BetaRawMessageStreamEventUnion{ + // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStop]()), + // Index: 0, // TODO: which index to use? + //}) + } + } + // + //if !foundToolCall { + if err := es.TrySend(streamCtx, event, event.RawJSON()); err != nil { + b.logConnectionError(ctx, err, "sending event") + if isConnectionError(err) { + return // Stop processing if client disconnected } } + + if len(appendedToolMessages) > 0 { + toolMsg := anthropic.NewBetaUserMessage(appendedToolMessages...) + + // + // + // + // + // + // TODO: ok, next steps... + // once we get message_stop, that's IT, FINITO. we have to pass all of the messages seen until now into a new request (just loop) + // and include the tool result as a new user message. + // this will mean we will loop continuously until we hit a message_stop and no tool results are pending. (any other stopping conditions?) + // that's when we'll let the loop exit and the request complete. + // + // + // + // + // + // + } + + //} else { + // fmt.Printf("[ignored, tool call found] %s\n", event.RawJSON()) + //} } -outer: + //if foundToolCall { + // for _, c := range message.Content { + // switch c.AsAny().(type) { + // case anthropic.BetaToolUseBlock: + // fn := c.AsToolUse().Name + // // input := c.AsToolUse().Input + // + // var input GetCoordinatesInput + // raw := c.Input + // err = json.Unmarshal(raw, &input) + // if err != nil { + // b.logger.Error(ctx, "failed to send event", slog.Error(err)) + // goto outer + // } + // + // fmt.Printf("[tool] %s %+v\n", fn, input) + // resp := GetCoordinates(input.Location) + // out := fmt.Sprintf("The latitude is %.2f and longitude is %.2f.", resp.Lat, resp.Long) + // + // _, err = coderdClient.TrackToolUse(streamCtx, &proto.TrackToolUseRequest{ + // Model: string(message.Model), + // Input: map[string]string{ + // "location": input.Location, + // }, + // Tool: fn, + // }) + // if err != nil { + // b.logger.Error(ctx, "failed to track usage", slog.Error(err)) + // } + // + // // Start content block + // var textType ant_constant.Text + // if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ + // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStart]()), + // Index: 0, // TODO: which index to use? + // ContentBlock: anthropic.BetaRawContentBlockStartEventContentBlockUnion{ + // Type: string(textType.Default()), + // }, + // }); err != nil { + // b.logger.Error(ctx, "failed to send content block start event", slog.Error(err)) + // } + // + // // Send the tool result + // if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ + // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockDelta]()), + // Index: 0, // TODO: which index to use? + // Delta: anthropic.BetaRawMessageStreamEventUnionDelta{ + // Type: string(ant_constant.ValueOf[ant_constant.TextDelta]()), + // Text: out, + // }, + // }); err != nil { + // b.logger.Error(ctx, "failed to send content block delta event", slog.Error(err)) + // } + // + // // Stop content block + // if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ + // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStop]()), + // Index: 0, // TODO: which index to use? + // }); err != nil { + // b.logger.Error(ctx, "failed to send content block stop event", slog.Error(err)) + // } + // } + // } + //} + + //outer: for _, c := range message.Content { switch block := c.AsAny().(type) { case anthropic.BetaMCPToolUseBlock: @@ -716,7 +840,7 @@ outer: b.logger.Error(ctx, "anthropic stream error", slog.Error(streamErr)) b.setError(streamErr) if antErr := getAnthropicErrorResponse(streamErr); antErr != nil { - err = es.TrySend(streamCtx, antErr) + err = es.TrySend(streamCtx, antErr, "") if err != nil { b.logger.Error(ctx, "failed to send error", slog.Error(err)) } diff --git a/aibridged/mcp.go b/aibridged/mcp.go new file mode 100644 index 0000000000000..3ec14aacc3f93 --- /dev/null +++ b/aibridged/mcp.go @@ -0,0 +1,110 @@ +package aibridged + +import ( + "context" + "fmt" + + "cdr.dev/slog" + "github.com/anthropics/anthropic-sdk-go" + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/client/transport" + "github.com/mark3labs/mcp-go/mcp" + "golang.org/x/xerrors" +) + +type BridgeMCPProxy struct { + name string + client *client.Client + logger slog.Logger + foundTools []anthropic.BetaToolUnionParam +} + +const MCPProxyDelimiter = "_" + +func NewBridgeMCPProxy(name, serverURL, token string, logger slog.Logger) (*BridgeMCPProxy, error) { + mcpClient, err := client.NewStreamableHttpClient(serverURL, + transport.WithHTTPHeaders(map[string]string{ + "Authorization": "Bearer " + token, + })) + if err != nil { + return nil, xerrors.Errorf("create streamable http client: %w", err) + } + + return &BridgeMCPProxy{ + name: name, + client: mcpClient, + logger: logger, + }, nil +} + +func (b *BridgeMCPProxy) Init(ctx context.Context) error { + if err := b.client.Start(ctx); err != nil { + return xerrors.Errorf("start client: %w", err) + } + + tools, err := b.fetchMCPTools(ctx) + if err != nil { + return xerrors.Errorf("fetch tools: %w", err) + } + + b.foundTools = tools + return nil +} + +func (b *BridgeMCPProxy) ListTools() []anthropic.BetaToolUnionParam { + return b.foundTools +} + +func (b *BridgeMCPProxy) CallTool(ctx context.Context, name string, input any) (*mcp.CallToolResult, error) { + return b.client.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: name, + Arguments: input, + }, + }) +} + +func (b *BridgeMCPProxy) fetchMCPTools(ctx context.Context) ([]anthropic.BetaToolUnionParam, error) { + initReq := mcp.InitializeRequest{ + Params: mcp.InitializeParams{ + ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, + ClientInfo: mcp.Implementation{ + Name: "coder-ai-bridge", + Version: "0.0.1", + }, + }, + } + + result, err := b.client.Initialize(ctx, initReq) + if err != nil { + return nil, xerrors.Errorf("init MCP client: %w", err) + } + fmt.Println(result.ProtocolVersion) // TODO: remove. + + // Test tool listing + tools, err := b.client.ListTools(ctx, mcp.ListToolsRequest{}) + if err != nil { + return nil, xerrors.Errorf("list MCP tools: %w", err) + } + + out := make([]anthropic.BetaToolUnionParam, 0, len(tools.Tools)) + for _, tool := range tools.Tools { + out = append(out, anthropic.BetaToolUnionParam{ + OfTool: &anthropic.BetaToolParam{ + InputSchema: anthropic.BetaToolInputSchemaParam{ + Properties: tool.InputSchema.Properties, + Required: tool.InputSchema.Required, + }, + Name: fmt.Sprintf("%s%s%s", b.name, MCPProxyDelimiter, tool.Name), + Description: anthropic.String(tool.Description), + Type: anthropic.BetaToolTypeCustom, + }, + }) + } + + return out, nil +} + +func (b *BridgeMCPProxy) Close() { + // TODO: atomically close. +} diff --git a/aibridged/streaming.go b/aibridged/streaming.go index b104c9bfcb02a..b174ca02a2873 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -63,16 +63,22 @@ func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, stream EventS return case <-stream.Closed(): return - case payload, ok := <-stream.Events(): + case ev, ok := <-stream.Events(): if !ok { return } + + // TODO: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/implement-tool-use#example-of-successful-tool-result see "Important formatting requirements" + + + + // TODO: use logger, make configurable. //_, _ = fmt.Fprintf(os.Stderr, "[%s] %s", sessionID, payload) - _, _ = os.Stderr.Write(payload) + _, _ = os.Stderr.Write([]byte(fmt.Sprintf("[orig] %s\n[zero] %s\n[out] %s", ev.orig, ev.zero, ev.payload))) - _, err := w.Write(payload) + _, err := w.Write(ev.payload) if err != nil { if isConnectionError(err) { logger.Debug(ctx, "client disconnected during SSE write", slog.Error(err)) @@ -102,14 +108,20 @@ func flush(w http.ResponseWriter) { } type EventStreamer interface { - TrySend(ctx context.Context, data any, exclusions ...string) error - Events() <-chan []byte + TrySend(ctx context.Context, data any, input string, exclusions ...string) error + Events() <-chan event Close(ctx context.Context) error Closed() <-chan any } +type event struct { + payload []byte + zero []byte // Marshaling with zero-value elements omitted. + orig string +} + type eventStream struct { - eventsCh chan []byte + eventsCh chan event kind eventStreamProvider closedOnce sync.Once @@ -126,12 +138,12 @@ const ( func newEventStream(kind eventStreamProvider) *eventStream { return &eventStream{ kind: kind, - eventsCh: make(chan []byte), + eventsCh: make(chan event), closedCh: make(chan any), } } -func (s *eventStream) Events() <-chan []byte { +func (s *eventStream) Events() <-chan event { return s.eventsCh } @@ -139,7 +151,7 @@ func (s *eventStream) Closed() <-chan any { return s.closedCh } -func (s *eventStream) TrySend(ctx context.Context, data any, exclusions ...string) error { +func (s *eventStream) TrySend(ctx context.Context, data any, input string, exclusions ...string) error { // Save an unnecessary marshaling if possible. select { case <-ctx.Done(): @@ -161,20 +173,19 @@ func (s *eventStream) TrySend(ctx context.Context, data any, exclusions ...strin // out all the zero value objects in the response, with optional exclusions. payload, err = util.MarshalNoZero(data, exclusions...) default: - zero, _ := util.MarshalNoZero(data, exclusions...) - fmt.Printf("[zero] %s\n", zero) - payload, err = json.Marshal(data) } + zero, _ := util.MarshalNoZero(data, exclusions...) + if err != nil { return xerrors.Errorf("marshal payload: %w", err) } - return s.send(ctx, payload) + return s.send(ctx, payload, zero, input) } -func (s *eventStream) send(ctx context.Context, payload []byte) error { +func (s *eventStream) send(ctx context.Context, payload, zero []byte, input string) error { switch s.kind { case openAIEventStream: var buf bytes.Buffer @@ -209,7 +220,7 @@ func (s *eventStream) send(ctx context.Context, payload []byte) error { return ctx.Err() case <-s.closedCh: return xerrors.New("closed") - case s.eventsCh <- payload: + case s.eventsCh <- event{payload: payload, orig: input, zero: zero}: return nil } } @@ -219,7 +230,7 @@ func (s *eventStream) Close(ctx context.Context) error { s.closedOnce.Do(func() { switch s.kind { case openAIEventStream: - err := s.send(ctx, []byte("[DONE]")) + err := s.send(ctx, []byte("[DONE]"), nil, "") if err != nil { out = xerrors.Errorf("close stream: %w", err) } diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index b9b3709c5abac..79c440a70ca0b 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -56,7 +56,7 @@ func (s *Server) TrackToolUse(ctx context.Context, in *proto.TrackToolUseRequest return nil, xerrors.Errorf("marshal event: %w", err) } - err = s.store.InsertWormholeEvent(ctx, database.InsertWormholeEventParams{Event: raw, EventType: "tool_use"}) + err = s.store.InsertWormholeEvent(ctx, database.InsertWormholeEventParams{Event: raw, EventType: "tool_usage"}) if err != nil { return nil, xerrors.Errorf("store event: %w", err) } From c4a3cdeebeff14989fb236c370aef79447ebf396 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 9 Jul 2025 11:51:23 +0200 Subject: [PATCH 28/61] WIP: tool calling working!!!11!!!1!! Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 2 + aibridged/bridge.go | 494 +++++++++++++++++++---------------------- aibridged/mcp.go | 16 +- aibridged/streaming.go | 3 +- cli/server.go | 52 +++-- 5 files changed, 273 insertions(+), 294 deletions(-) diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 717f1398b2449..b056730bfa7e2 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -71,6 +71,8 @@ func New(rpcDialer Dialer, httpAddr string, logger slog.Logger, bridgeCfg coders initConnectionCh: make(chan struct{}), } + // TODO: improve error handling here; if this fails it prevents the whole server from starting up! + bridge, err := NewBridge(bridgeCfg, httpAddr, logger.Named("ai_bridge"), daemon.client) if err != nil { return nil, xerrors.Errorf("create new bridge server: %w", err) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 1ee2b87b0c4a2..98b185fc9f87a 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -1,6 +1,7 @@ package aibridged import ( + "bytes" "context" "encoding/json" "errors" @@ -8,6 +9,7 @@ import ( "io" "net" "net/http" + "net/http/httputil" "net/url" "os" "strings" @@ -88,23 +90,44 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli bridge.clientFn = clientFn bridge.logger = logger - const githubMCPName = "github" - githubMCP, err := NewBridgeMCPProxy(githubMCPName, "https://api.githubcopilot.com/mcp/", os.Getenv("GITHUB_MCP_TOKEN"), logger.Named("mcp-proxy")) + time.Sleep(time.Second * 3) + + const ( + githubMCPName = "github" + coderMCPName = "coder" + ) + githubMCP, err := NewBridgeMCPProxy(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ + "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), + }, logger.Named("mcp-proxy-github")) if err != nil { return nil, xerrors.Errorf("github MCP proxy setup: %w", err) } + coderMCP, err := NewBridgeMCPProxy(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ + "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), + // This is necessary to even access the MCP endpoint. + "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), + }, logger.Named("mcp-proxy-coder")) + if err != nil { + return nil, xerrors.Errorf("coder MCP proxy setup: %w", err) + } bridge.mcpProxies = map[string]*BridgeMCPProxy{ githubMCPName: githubMCP, + coderMCPName: coderMCP, } - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() var eg errgroup.Group + //eg.Go(func() error { + // return githubMCP.Init(ctx) + //}) eg.Go(func() error { - return githubMCP.Init(ctx) + return coderMCP.Init(ctx) }) + // This must block requests until MCP proxies are setup. if err := eg.Wait(); err != nil { return nil, xerrors.Errorf("MCP proxy init: %w", err) } @@ -226,7 +249,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } }() - BasicSSESender(streamCtx, sessionID, es, b.logger.Named("sse-sender")).ServeHTTP(w, r) + BasicSSESender(streamCtx, sessionID, "", es, b.logger.Named("sse-sender")).ServeHTTP(w, r) }() session := NewOpenAISession() @@ -413,6 +436,26 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } } +func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { + reqOut, _ := httputil.DumpRequest(req, true) + + // Forward the request to the next handler + res, err = next(req) + + isSmallFastModel := strings.Contains(string(reqOut), "3-5-haiku") + if isSmallFastModel { + return res, err + } + + fmt.Printf("[req] %s\n", reqOut) + + // Handle stuff after the request + respOut, _ := httputil.DumpResponse(res, true) + fmt.Printf("[resp] %s\n", respOut) + + return res, err +} + func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { sessionID := uuid.New() b.logger.Info(r.Context(), "Anthropic request started", slog.F("sessionID", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) @@ -516,10 +559,17 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } + messages := in.BetaMessageNewParams + + var opts []option.RequestOption + if reqBetaHeader := r.Header.Get("anthropic-beta"); strings.TrimSpace(reqBetaHeader) != "" { + opts = append(opts, option.WithHeader("anthropic-beta", reqBetaHeader)) + } + // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") - client := anthropic.NewClient(option.WithHeader("anthropic-beta", anthropic.AnthropicBetaMCPClient2025_04_04)) - if !in.UseStreaming() { - msg, err := client.Beta.Messages.New(ctx, in.BetaMessageNewParams) + client := anthropic.NewClient(opts...) + if !in.UseStreaming() || isSmallFastModel { + msg, err := client.Beta.Messages.New(ctx, messages) if err != nil { if antErr := getAnthropicErrorResponse(err); antErr != nil { b.setError(antErr) @@ -555,7 +605,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { return } - streamCtx, streamCancel := context.WithCancelCause(ctx) + streamCtx, streamCancel := context.WithCancelCause(r.Context()) defer streamCancel(xerrors.New("deferred")) es := newEventStream(anthropicEventStream) @@ -577,298 +627,212 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } }() - BasicSSESender(streamCtx, sessionID, es, b.logger.Named("sse-sender")).ServeHTTP(w, r) + BasicSSESender(streamCtx, sessionID, string(in.Model), es, b.logger.Named("sse-sender")).ServeHTTP(w, r) }() - stream := client.Beta.Messages.NewStreaming(streamCtx, in.BetaMessageNewParams) + isFirst := true + for { + newStream: + stream := client.Beta.Messages.NewStreaming(streamCtx, messages) - var events []anthropic.BetaRawMessageStreamEventUnion - var message anthropic.BetaMessage - for stream.Next() { - event := stream.Current() - events = append(events, event) + var events []anthropic.BetaRawMessageStreamEventUnion + var message anthropic.BetaMessage + pendingToolCalls := make(map[string]string) - //fmt.Printf("[provider] %s\n", event.RawJSON()) + for stream.Next() { + event := stream.Current() + events = append(events, event) - if err := message.Accumulate(event); err != nil { - b.logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) - http.Error(w, "failed to proxy request", http.StatusInternalServerError) - return - } + if err := message.Accumulate(event); err != nil { + b.logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) + http.Error(w, "failed to proxy request", http.StatusInternalServerError) + return + } - // Regular tool call: - // [zero] {"type":"content_block_stop"} - //[zero] {"content_block":{"id":"toolu_015YCpDbjWuSbcKGfDRWR1bD","name":"get_coordinates","type":"tool_use"},"index":1,"type":"content_block_start"} - //[zero] {"delta":{"type":"input_json_delta"},"index":1,"type":"content_block_delta"} - - var appendedToolMessages []anthropic.BetaContentBlockParamUnion - - switch event.Type { - case string(constant.ValueOf[ant_constant.ContentBlockStart]()): // Have to do this because otherwise content_block_delta and content_block_start both match the type anthropic.BetaRawContentBlockStartEvent - switch block := event.AsContentBlockStart().ContentBlock.AsAny().(type) { - case anthropic.BetaToolUseBlock: - // if block.Name == "get_coordinates" { - // foundToolCall = true // Ensure no more events get sent after this point since our injected tool needs to be called. - // } - fmt.Printf("[event] %s\n[tool] %s %+v\n\n", event.RawJSON(), block.Name, block.Input) - - serverName, toolName, found := strings.Cut(block.Name, MCPProxyDelimiter) - if !found { - // Not an MCP proxy call, don't do anything. + // Tool-related handling. + switch event.Type { + case string(constant.ValueOf[ant_constant.ContentBlockStart]()): // Have to do this because otherwise content_block_delta and content_block_start both match the type anthropic.BetaRawContentBlockStartEvent + switch block := event.AsContentBlockStart().ContentBlock.AsAny().(type) { + case anthropic.BetaToolUseBlock: + pendingToolCalls[block.Name] = block.ID + // Don't relay this event back, otherwise the client will try invoke the tool as well. continue } - - _, err = coderdClient.TrackToolUse(streamCtx, &proto.TrackToolUseRequest{ - Model: string(message.Model), - Input: map[string]string{}, // TODO: input. - Tool: toolName, - }) - if err != nil { - b.logger.Error(ctx, "failed to track usage", slog.Error(err)) + case string(constant.ValueOf[ant_constant.ContentBlockDelta]()): + if len(pendingToolCalls) > 0 { + // We're busy with a tool call, don't relay this event back. + continue + } + case string(constant.ValueOf[ant_constant.ContentBlockStop]()): + if len(pendingToolCalls) > 0 { + // We're busy with a tool call, don't relay this event back. + continue } + case string(ant_constant.ValueOf[ant_constant.MessageStart]()): + if !isFirst { + // don't send message_start unless first message. + continue + } + case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): + // Don't relay message_delta events which indicate tool use. + //if event.AsMessageDelta().Delta.StopReason == anthropic.BetaStopReasonToolUse { + // don't send message_start unless first message. + continue + //} + case string(ant_constant.ValueOf[ant_constant.ToolResult]()), string(ant_constant.ValueOf[ant_constant.ToolUse]()): + continue - res, err := b.mcpProxies[serverName].CallTool(streamCtx, toolName, block.Input) - if err != nil { - // TODO: + // Don't send message_stop until all tools have been called. + case string(ant_constant.ValueOf[ant_constant.MessageStop]()): + // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! + if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ + MsgId: message.ID, + Model: string(message.Model), + InputTokens: message.Usage.InputTokens, + OutputTokens: message.Usage.OutputTokens, + Other: map[string]int64{ + "web_search_requests": message.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": message.Usage.CacheCreationInputTokens, + "cache_read_input": message.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, + }, + }); err != nil { + b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) } - var out strings.Builder - if err := json.NewEncoder(&out).Encode(res); err != nil { - b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) - continue + if len(pendingToolCalls) > 0 { + // Append the whole message from this stream as context since we'll be sending a new request with the tool results. + messages.Messages = append(messages.Messages, message.ToParam()) } - //// Start content block - //var textType ant_constant.Text - //appendedToolMessages = append(appendedToolMessages, anthropic.BetaRawMessageStreamEventUnion{ - // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStart]()), - // Index: 0, // TODO: which index to use? - // ContentBlock: anthropic.BetaRawContentBlockStartEventContentBlockUnion{ - // Type: string(textType.Default()), - // }, - //}) - - for _, content := range res.Content { - switch cb := content.(type) { - case mcp.TextContent: - appendedToolMessages = append(appendedToolMessages, - anthropic.NewBetaToolResultBlock(block.ID, cb.Text, false)) - default: - // Not supported. - appendedToolMessages = append(appendedToolMessages, - anthropic.NewBetaToolResultBlock(block.ID, out.String(), false)) + for name, id := range pendingToolCalls { + serverName, toolName, found := strings.Cut(name, MCPProxyDelimiter) + if !found { + // Not an MCP proxy call, don't do anything. + continue } - } - //// Start content block - //var textType ant_constant.Text - //appendedToolMessages = append(appendedToolMessages, anthropic.BetaRawMessageStreamEventUnion{ - // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStart]()), - // Index: 0, // TODO: which index to use? - // ContentBlock: anthropic.BetaRawContentBlockStartEventContentBlockUnion{ - // Type: string(textType.Default()), - // }, - //}) - // - //// Send the tool result - //appendedToolMessages = append(appendedToolMessages, anthropic.BetaRawMessageStreamEventUnion{ - // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockDelta]()), - // Index: 0, // TODO: which index to use? - // Delta: anthropic.BetaRawMessageStreamEventUnionDelta{ - // Type: string(ant_constant.ValueOf[ant_constant.TextDelta]()), - // Text: out.String(), - // }, - //}) - // - //// Stop content block - //appendedToolMessages = append(appendedToolMessages, anthropic.BetaRawMessageStreamEventUnion{ - // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStop]()), - // Index: 0, // TODO: which index to use? - //}) - } - } - // - //if !foundToolCall { - if err := es.TrySend(streamCtx, event, event.RawJSON()); err != nil { - b.logConnectionError(ctx, err, "sending event") - if isConnectionError(err) { - return // Stop processing if client disconnected - } - } + var ( + input any + foundTool bool + foundTools int + ) + for _, block := range message.Content { + switch variant := block.AsAny().(type) { + case anthropic.BetaToolUseBlock: + foundTools++ + if variant.Name == name { + input = variant.Input + foundTool = true + } + } + } - if len(appendedToolMessages) > 0 { - toolMsg := anthropic.NewBetaUserMessage(appendedToolMessages...) - - // - // - // - // - // - // TODO: ok, next steps... - // once we get message_stop, that's IT, FINITO. we have to pass all of the messages seen until now into a new request (just loop) - // and include the tool result as a new user message. - // this will mean we will loop continuously until we hit a message_stop and no tool results are pending. (any other stopping conditions?) - // that's when we'll let the loop exit and the request complete. - // - // - // - // - // - // - } + if !foundTool { + b.logger.Error(ctx, "failed to find tool input", slog.F("tool_name", name), slog.F("found_tools", foundTools)) + continue + } - //} else { - // fmt.Printf("[ignored, tool call found] %s\n", event.RawJSON()) - //} - } + var ( + serialized map[string]string + buf bytes.Buffer + ) + _ = json.NewEncoder(&buf).Encode(input) + _ = json.NewDecoder(&buf).Decode(&serialized) - //if foundToolCall { - // for _, c := range message.Content { - // switch c.AsAny().(type) { - // case anthropic.BetaToolUseBlock: - // fn := c.AsToolUse().Name - // // input := c.AsToolUse().Input - // - // var input GetCoordinatesInput - // raw := c.Input - // err = json.Unmarshal(raw, &input) - // if err != nil { - // b.logger.Error(ctx, "failed to send event", slog.Error(err)) - // goto outer - // } - // - // fmt.Printf("[tool] %s %+v\n", fn, input) - // resp := GetCoordinates(input.Location) - // out := fmt.Sprintf("The latitude is %.2f and longitude is %.2f.", resp.Lat, resp.Long) - // - // _, err = coderdClient.TrackToolUse(streamCtx, &proto.TrackToolUseRequest{ - // Model: string(message.Model), - // Input: map[string]string{ - // "location": input.Location, - // }, - // Tool: fn, - // }) - // if err != nil { - // b.logger.Error(ctx, "failed to track usage", slog.Error(err)) - // } - // - // // Start content block - // var textType ant_constant.Text - // if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ - // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStart]()), - // Index: 0, // TODO: which index to use? - // ContentBlock: anthropic.BetaRawContentBlockStartEventContentBlockUnion{ - // Type: string(textType.Default()), - // }, - // }); err != nil { - // b.logger.Error(ctx, "failed to send content block start event", slog.Error(err)) - // } - // - // // Send the tool result - // if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ - // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockDelta]()), - // Index: 0, // TODO: which index to use? - // Delta: anthropic.BetaRawMessageStreamEventUnionDelta{ - // Type: string(ant_constant.ValueOf[ant_constant.TextDelta]()), - // Text: out, - // }, - // }); err != nil { - // b.logger.Error(ctx, "failed to send content block delta event", slog.Error(err)) - // } - // - // // Stop content block - // if err := es.TrySend(streamCtx, anthropic.BetaRawMessageStreamEventUnion{ - // Type: string(ant_constant.ValueOf[ant_constant.ContentBlockStop]()), - // Index: 0, // TODO: which index to use? - // }); err != nil { - // b.logger.Error(ctx, "failed to send content block stop event", slog.Error(err)) - // } - // } - // } - //} + fmt.Printf("[event] %s\n[tool(%q)] %s %+v\n\n", event.RawJSON(), id, name, input) - //outer: - for _, c := range message.Content { - switch block := c.AsAny().(type) { - case anthropic.BetaMCPToolUseBlock: + _, err = coderdClient.TrackToolUse(streamCtx, &proto.TrackToolUseRequest{ + Model: string(message.Model), + Input: serialized, + Tool: toolName, + }) + if err != nil { + b.logger.Error(ctx, "failed to track usage", slog.Error(err)) + } - var input map[string]string - if err := json.Unmarshal([]byte(block.JSON.Input.Raw()), &input); err != nil { - b.logger.Error(ctx, "failed to marshal tool input", slog.Error(err)) - continue - } + res, err := b.mcpProxies[serverName].CallTool(streamCtx, toolName, input) + if err != nil { + // TODO: + } - if _, err = coderdClient.TrackToolUse(streamCtx, &proto.TrackToolUseRequest{ - Tool: block.Name, - Model: string(message.Model), - Input: input, - }); err != nil { - b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) + var out strings.Builder + if err := json.NewEncoder(&out).Encode(res); err != nil { + b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) + continue + } + + for _, content := range res.Content { + switch cb := content.(type) { + case mcp.TextContent: + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, cb.Text, false)), + ) + default: + // Not supported. + b.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb)), slog.F("json", out.String())) + } + } + } + + if len(pendingToolCalls) > 0 { + // Causes a new stream to be run with updated messages. + isFirst = false + pendingToolCalls = nil + goto newStream + } } - case anthropic.BetaMCPToolResultBlock: - for _, res := range block.Content.AsBetaMCPToolResultBlockContent() { - // TODO: store results? - x := res.Text - fmt.Println(x) + + if err := es.TrySend(streamCtx, event, event.RawJSON()); err != nil { + b.logConnectionError(ctx, err, "sending event") + if isConnectionError(err) { + return // Stop processing if client disconnected + } } } - } - // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! - if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ - MsgId: message.ID, - Model: string(message.Model), - InputTokens: message.Usage.InputTokens, - OutputTokens: message.Usage.OutputTokens, - Other: map[string]int64{ - "web_search_requests": message.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": message.Usage.CacheCreationInputTokens, - "cache_read_input": message.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, - }, - }); err != nil { - b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) - } + var streamErr error + if streamErr = stream.Err(); streamErr != nil { + if isConnectionError(streamErr) { + b.logger.Warn(ctx, "upstream connection closed", slog.Error(streamErr)) + } - var streamErr error - if streamErr = stream.Err(); streamErr != nil { - if isConnectionError(streamErr) { - b.logger.Warn(ctx, "upstream connection closed", slog.Error(streamErr)) - } + b.logger.Error(ctx, "anthropic stream error", slog.Error(streamErr)) + b.setError(streamErr) + if antErr := getAnthropicErrorResponse(streamErr); antErr != nil { + err = es.TrySend(streamCtx, antErr, "") + if err != nil { + b.logger.Error(ctx, "failed to send error", slog.Error(err)) + } - b.logger.Error(ctx, "anthropic stream error", slog.Error(streamErr)) - b.setError(streamErr) - if antErr := getAnthropicErrorResponse(streamErr); antErr != nil { - err = es.TrySend(streamCtx, antErr, "") - if err != nil { - b.logger.Error(ctx, "failed to send error", slog.Error(err)) + http.Error(w, antErr.Error.Message, ProxyErrCode) + } else { + http.Error(w, streamErr.Error(), ProxyErrCode) } + } - http.Error(w, antErr.Error.Message, ProxyErrCode) - } else { - http.Error(w, streamErr.Error(), ProxyErrCode) + err = es.Close(streamCtx) + if err != nil { + b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) } - } - err = es.Close(streamCtx) - if err != nil { - b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) - } + wg.Wait() - wg.Wait() + // Ensure we flush all the remaining data before ending. + flush(w) - // Ensure we flush all the remaining data before ending. - flush(w) + if err != nil || streamErr != nil { + streamCancel(xerrors.Errorf("stream err: %w", err)) + } else { + streamCancel(xerrors.New("gracefully done")) + } - if err != nil || streamErr != nil { - streamCancel(xerrors.Errorf("stream err: %w", err)) - } else { - streamCancel(xerrors.New("gracefully done")) - } + select { + case <-streamCtx.Done(): + } - select { - case <-streamCtx.Done(): + break } } diff --git a/aibridged/mcp.go b/aibridged/mcp.go index 3ec14aacc3f93..9b766463210b2 100644 --- a/aibridged/mcp.go +++ b/aibridged/mcp.go @@ -21,11 +21,19 @@ type BridgeMCPProxy struct { const MCPProxyDelimiter = "_" -func NewBridgeMCPProxy(name, serverURL, token string, logger slog.Logger) (*BridgeMCPProxy, error) { +func NewBridgeMCPProxy(name, serverURL string, headers map[string]string, logger slog.Logger) (*BridgeMCPProxy, error) { + //ts := transport.NewMemoryTokenStore() + //if err := ts.SaveToken(&transport.Token{ + // AccessToken: token, + //}); err != nil { + // return nil, xerrors.Errorf("save token: %w", err) + //} + mcpClient, err := client.NewStreamableHttpClient(serverURL, - transport.WithHTTPHeaders(map[string]string{ - "Authorization": "Bearer " + token, - })) + transport.WithHTTPHeaders(headers)) + //transport.WithHTTPOAuth(transport.OAuthConfig{ + // TokenStore: ts, + //})) if err != nil { return nil, xerrors.Errorf("create streamable http client: %w", err) } diff --git a/aibridged/streaming.go b/aibridged/streaming.go index b174ca02a2873..f469bea6a0fb2 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -43,7 +43,7 @@ func isConnectionError(err error) bool { // BasicSSESender was implemented to overcome httpapi.ServerSentEventSender's odd design choices. For example, it doesn't // write "event: data" for every data event (it's unnecessary, and breaks some AI tools' parsing of the SSE stream). -func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, stream EventStreamer, logger slog.Logger) http.HandlerFunc { +func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, model string, stream EventStreamer, logger slog.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -60,6 +60,7 @@ func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, stream EventS case <-outerCtx.Done(): return case <-ctx.Done(): + fmt.Printf("request done for model %s, reason: %q\n", model, ctx.Err()) return case <-stream.Closed(): return diff --git a/cli/server.go b/cli/server.go index 9cccad3a78a21..cd88dad1bb30e 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1038,30 +1038,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // avoid leaving dangling goroutines waiting for the // channel to be consumed. errCh = make(chan error, 1) - aiBridgeDaemons := make([]*aibridged.Server, 0) - defer func() { - // We have no graceful shutdown of aiBridgeDaemons - // here because that's handled at the end of main, this - // is here in case the program exits early. - for _, daemon := range aiBridgeDaemons { - _ = daemon.Close() - } - }() - - // Built in aibridge daemons. - for i := int64(0); i < vals.AI.BridgeConfig.Daemons.Value(); i++ { - suffix := fmt.Sprintf("%d", i) - // The suffix is added to the hostname, so we may need to trim to fit into - // the 64 character limit. - hostname := stringutil.Truncate(cliutil.Hostname(), 63-len(suffix)) - name := fmt.Sprintf("%s-%s", hostname, suffix) - daemon, err := newAIBridgeDaemon(ctx, coderAPI, name, vals.AI.BridgeConfig) - if err != nil { - return xerrors.Errorf("create provisioner daemon: %w", err) - } - aiBridgeDaemons = append(aiBridgeDaemons, daemon) - } - coderAPI.AIBridgeDaemons = aiBridgeDaemons shutdownConnsCtx, shutdownConns := context.WithCancel(ctx) defer shutdownConns() @@ -1130,6 +1106,32 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } }() + // TODO: this shouldn't block. + aiBridgeDaemons := make([]*aibridged.Server, 0) + defer func() { + // We have no graceful shutdown of aiBridgeDaemons + // here because that's handled at the end of main, this + // is here in case the program exits early. + for _, daemon := range aiBridgeDaemons { + _ = daemon.Close() + } + }() + + // Built in aibridge daemons. + for i := int64(0); i < vals.AI.BridgeConfig.Daemons.Value(); i++ { + suffix := fmt.Sprintf("%d", i) + // The suffix is added to the hostname, so we may need to trim to fit into + // the 64 character limit. + hostname := stringutil.Truncate(cliutil.Hostname(), 63-len(suffix)) + name := fmt.Sprintf("%s-%s", hostname, suffix) + daemon, err := newAIBridgeDaemon(ctx, coderAPI, name, vals.AI.BridgeConfig) + if err != nil { + return xerrors.Errorf("create provisioner daemon: %w", err) + } + aiBridgeDaemons = append(aiBridgeDaemons, daemon) + } + coderAPI.AIBridgeDaemons = aiBridgeDaemons + // Updates the systemd status from activating to activated. _, err = daemon.SdNotify(false, daemon.SdNotifyReady) if err != nil { @@ -1576,6 +1578,8 @@ func newProvisionerDaemon( func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string, bridgeCfg codersdk.AIBridgeConfig) (*aibridged.Server, error) { httpAddr := "0.0.0.0:0" // TODO: configurable. + + return aibridged.New(func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { // This debounces calls to listen every second. // TODO: is this true / necessary? From e74ffe3ef6d8ff37c7ccdc7365838701f290f57e Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 9 Jul 2025 17:04:10 +0200 Subject: [PATCH 29/61] tool use tracking & complex tool result handling Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 6 +- aibridged/bridge.go | 356 +++++++++++++++------- aibridged/mcp.go | 36 ++- aibridged/proto/aibridged.pb.go | 145 +++++---- aibridged/proto/aibridged.proto | 7 +- aibridged/proto/aibridged_drpc.pb.go | 28 +- coderd/aibridgedserver/aibridgedserver.go | 4 +- 7 files changed, 373 insertions(+), 209 deletions(-) diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index b056730bfa7e2..b120c05bc8406 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -194,9 +194,9 @@ func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPrompt return out, nil } -func (s *Server) TrackToolUse(ctx context.Context, in *proto.TrackToolUseRequest) (*proto.TrackToolUseResponse, error) { - out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackToolUseResponse, error) { - return client.TrackToolUse(ctx, in) +func (s *Server) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageRequest) (*proto.TrackToolUsageResponse, error) { + out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackToolUsageResponse, error) { + return client.TrackToolUsage(ctx, in) }) if err != nil { return nil, err diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 98b185fc9f87a..5df8f80e18c20 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -69,7 +69,7 @@ type Bridge struct { logger slog.Logger lastErr error - mcpProxies map[string]*BridgeMCPProxy + mcpBridges map[string]*MCPToolBridge } func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) (*Bridge, error) { @@ -96,22 +96,22 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli githubMCPName = "github" coderMCPName = "coder" ) - githubMCP, err := NewBridgeMCPProxy(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ + githubMCP, err := NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), - }, logger.Named("mcp-proxy-github")) + }, logger.Named("mcp-bridge-github")) if err != nil { - return nil, xerrors.Errorf("github MCP proxy setup: %w", err) + return nil, xerrors.Errorf("github MCP bridge setup: %w", err) } - coderMCP, err := NewBridgeMCPProxy(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ - "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), + coderMCP, err := NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ + "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), // This is necessary to even access the MCP endpoint. "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), - }, logger.Named("mcp-proxy-coder")) + }, logger.Named("mcp-bridge-coder")) if err != nil { - return nil, xerrors.Errorf("coder MCP proxy setup: %w", err) + return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) } - bridge.mcpProxies = map[string]*BridgeMCPProxy{ + bridge.mcpBridges = map[string]*MCPToolBridge{ githubMCPName: githubMCP, coderMCPName: coderMCP, } @@ -120,9 +120,9 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli defer cancel() var eg errgroup.Group - //eg.Go(func() error { - // return githubMCP.Init(ctx) - //}) + eg.Go(func() error { + return githubMCP.Init(ctx) + }) eg.Go(func() error { return coderMCP.Init(ctx) }) @@ -436,7 +436,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } } -func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { +func LoggingMiddleware(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { reqOut, _ := httputil.DumpRequest(req, true) // Forward the request to the next handler @@ -518,30 +518,9 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { InputSchema: GetCoordinatesInputSchema, }}) - for _, proxy := range b.mcpProxies { + for _, proxy := range b.mcpBridges { in.Tools = append(in.Tools, proxy.ListTools()...) } - // in.Tools = append(in.Tools, b.fetchMCPTools("", "")...) - - //// TODO: fetch instead with lib and inject into tools since OAI doesn't support MCP. - //in.MCPServers = []anthropic.BetaRequestMCPServerURLDefinitionParam{ - // { - // URL: "https://api.githubcopilot.com/mcp/", - // Name: "github", - // AuthorizationToken: param.NewOpt(os.Getenv("GITHUB_MCP_TOKEN")), - // ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{ - // Enabled: anthropic.Bool(true), - // }, - // }, - // { - // URL: "https://dev.coder.com/api/experimental/mcp/http", - // Name: "coder", - // AuthorizationToken: param.NewOpt(os.Getenv("CODER_MCP_TOKEN")), - // ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{ - // Enabled: anthropic.Bool(true), - // }, - // }, - //} // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). isSmallFastModel := strings.Contains(string(in.Model), "3-5-haiku") @@ -561,6 +540,14 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { messages := in.BetaMessageNewParams + // Note: Parallel tool calls are disabled in the processing loop to avoid tool_use/tool_result block mismatches + messages.ToolChoice = anthropic.BetaToolChoiceUnionParam{ + OfAny: &anthropic.BetaToolChoiceAnyParam{ + Type: "auto", + DisableParallelToolUse: anthropic.Bool(true), + }, + } + var opts []option.RequestOption if reqBetaHeader := r.Header.Get("anthropic-beta"); strings.TrimSpace(reqBetaHeader) != "" { opts = append(opts, option.WithHeader("anthropic-beta", reqBetaHeader)) @@ -637,6 +624,8 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { var events []anthropic.BetaRawMessageStreamEventUnion var message anthropic.BetaMessage + var lastToolName string + pendingToolCalls := make(map[string]string) for stream.Next() { @@ -650,37 +639,51 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } // Tool-related handling. + + // TODO: this should *ignore* built-in tools; so ONLY do this for injected tooling. + switch event.Type { case string(constant.ValueOf[ant_constant.ContentBlockStart]()): // Have to do this because otherwise content_block_delta and content_block_start both match the type anthropic.BetaRawContentBlockStartEvent switch block := event.AsContentBlockStart().ContentBlock.AsAny().(type) { case anthropic.BetaToolUseBlock: - pendingToolCalls[block.Name] = block.ID - // Don't relay this event back, otherwise the client will try invoke the tool as well. - continue + lastToolName = block.Name + + if b.isInjectedTool(block.Name) { + pendingToolCalls[block.Name] = block.ID + // Don't relay this event back, otherwise the client will try invoke the tool as well. + continue + } + default: + fmt.Printf("[%s] %s\n", event.Type, event.RawJSON()) } case string(constant.ValueOf[ant_constant.ContentBlockDelta]()): - if len(pendingToolCalls) > 0 { + if len(pendingToolCalls) > 0 && b.isInjectedTool(lastToolName) { // We're busy with a tool call, don't relay this event back. continue } case string(constant.ValueOf[ant_constant.ContentBlockStop]()): - if len(pendingToolCalls) > 0 { + // Reset the tool name + isInjected := b.isInjectedTool(lastToolName) + lastToolName = "" + + if len(pendingToolCalls) > 0 && isInjected { // We're busy with a tool call, don't relay this event back. continue } case string(ant_constant.ValueOf[ant_constant.MessageStart]()): if !isFirst { - // don't send message_start unless first message. + // Don't send message_start unless first message! + // We're sending multiple messages back and forth with the API, but from the client's perspective + // they're just expecting a single message. continue } case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): // Don't relay message_delta events which indicate tool use. - //if event.AsMessageDelta().Delta.StopReason == anthropic.BetaStopReasonToolUse { - // don't send message_start unless first message. - continue + // if event.AsMessageDelta().Delta.StopReason == anthropic.BetaStopReasonToolUse { + if len(pendingToolCalls) > 0 && b.isInjectedTool(lastToolName) { + continue + } //} - case string(ant_constant.ValueOf[ant_constant.ToolResult]()), string(ant_constant.ValueOf[ant_constant.ToolUse]()): - continue // Don't send message_stop until all tools have been called. case string(ant_constant.ValueOf[ant_constant.MessageStop]()): @@ -704,83 +707,205 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if len(pendingToolCalls) > 0 { // Append the whole message from this stream as context since we'll be sending a new request with the tool results. messages.Messages = append(messages.Messages, message.ToParam()) - } - for name, id := range pendingToolCalls { - serverName, toolName, found := strings.Cut(name, MCPProxyDelimiter) - if !found { - // Not an MCP proxy call, don't do anything. - continue - } + for name, id := range pendingToolCalls { + serverName, toolName, found := parseToolName(name) + if !found { + // Not an MCP proxy call, don't do anything. + continue + } - var ( - input any - foundTool bool - foundTools int - ) - for _, block := range message.Content { - switch variant := block.AsAny().(type) { - case anthropic.BetaToolUseBlock: - foundTools++ - if variant.Name == name { - input = variant.Input - foundTool = true + var ( + input any + foundTool bool + foundTools int + ) + for _, block := range message.Content { + switch variant := block.AsAny().(type) { + case anthropic.BetaToolUseBlock: + foundTools++ + if variant.Name == name { + input = variant.Input + foundTool = true + } } } - } - if !foundTool { - b.logger.Error(ctx, "failed to find tool input", slog.F("tool_name", name), slog.F("found_tools", foundTools)) - continue - } + if !foundTool { + b.logger.Error(ctx, "failed to find tool input", slog.F("tool_name", name), slog.F("found_tools", foundTools)) + continue + } - var ( - serialized map[string]string - buf bytes.Buffer - ) - _ = json.NewEncoder(&buf).Encode(input) - _ = json.NewDecoder(&buf).Decode(&serialized) + var ( + serialized map[string]string + buf bytes.Buffer + ) + _ = json.NewEncoder(&buf).Encode(input) + _ = json.NewDecoder(&buf).Decode(&serialized) + + fmt.Printf("[event] %s\n[tool(%q)] %s %+v\n\n", event.RawJSON(), id, name, input) + + _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ + Model: string(message.Model), + Input: serialized, + Tool: toolName, + Injected: true, + }) + if err != nil { + b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + } - fmt.Printf("[event] %s\n[tool(%q)] %s %+v\n\n", event.RawJSON(), id, name, input) + res, err := b.mcpBridges[serverName].CallTool(streamCtx, toolName, input) + if err != nil { + // Always provide a tool_result even if the tool call failed + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, fmt.Sprintf("Error calling tool: %v", err), true)), + ) + continue + } - _, err = coderdClient.TrackToolUse(streamCtx, &proto.TrackToolUseRequest{ - Model: string(message.Model), - Input: serialized, - Tool: toolName, - }) - if err != nil { - b.logger.Error(ctx, "failed to track usage", slog.Error(err)) - } + var out strings.Builder + if err := json.NewEncoder(&out).Encode(res); err != nil { + b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) + // Always provide a tool_result even if encoding failed + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, fmt.Sprintf("Error encoding tool response: %v", err), true)), + ) + continue + } - res, err := b.mcpProxies[serverName].CallTool(streamCtx, toolName, input) - if err != nil { - // TODO: - } + // Ensure at least one tool_result is always added for each tool_use + toolResult := anthropic.BetaContentBlockParamUnion{ + OfToolResult: &anthropic.BetaToolResultBlockParam{ + ToolUseID: id, + IsError: anthropic.Bool(false), + }, + } - var out strings.Builder - if err := json.NewEncoder(&out).Encode(res); err != nil { - b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) - continue - } + var hasValidResult bool + for _, content := range res.Content { + + switch cb := content.(type) { + case mcp.TextContent: + //messages.Messages = append(messages.Messages, + // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, cb.Text, false)), + //) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: cb.Text, + }, + }) + + hasValidResult = true + case mcp.EmbeddedResource: + // Handle embedded resource based on its type + switch resource := cb.Resource.(type) { + case mcp.TextResourceContents: + // For text resources, include the text content + val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", + resource.MIMEType, resource.URI, resource.Text) + //messages.Messages = append(messages.Messages, + // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, val, false)), + //) + + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: val, + }, + }) + hasValidResult = true + case mcp.BlobResourceContents: + // For blob resources, include the base64 data with MIME type info + val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", + resource.MIMEType, resource.URI, resource.Blob) + //messages.Messages = append(messages.Messages, + // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, val, false)), + //) + + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: val, + }, + }) + hasValidResult = true + default: + b.logger.Error(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource))) + //messages.Messages = append(messages.Messages, + // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, "Error: unknown embedded resource type", true)), + //) + + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: unknown embedded resource type", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + hasValidResult = true + } + default: + // Not supported - but we must still provide a tool_result to match the tool_use + b.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb)), slog.F("json", out.String())) + //messages.Messages = append(messages.Messages, + // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, "Error: unsupported tool result type", true)), + //) + + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: unsupported tool result type", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + hasValidResult = true + } + } - for _, content := range res.Content { - switch cb := content.(type) { - case mcp.TextContent: - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, cb.Text, false)), - ) - default: - // Not supported. - b.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb)), slog.F("json", out.String())) + // If no content was processed, still add a tool_result + if !hasValidResult { + //messages.Messages = append(messages.Messages, + // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, "Error: no valid tool result content", true)), + //) + + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: no valid tool result content", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + } + + if len(toolResult.OfToolResult.Content) > 0 { + messages.Messages = append(messages.Messages, anthropic.NewBetaUserMessage(toolResult)) } } - } - if len(pendingToolCalls) > 0 { // Causes a new stream to be run with updated messages. isFirst = false - pendingToolCalls = nil goto newStream + } else { + // Find all the non-injected tools and track their uses. + for _, block := range message.Content { + switch variant := block.AsAny().(type) { + case anthropic.BetaToolUseBlock: + if b.isInjectedTool(variant.Name) { + continue + } + + var ( + serialized map[string]string + buf bytes.Buffer + ) + _ = json.NewEncoder(&buf).Encode(variant.Input) + _ = json.NewDecoder(&buf).Decode(&serialized) + _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ + Model: string(message.Model), + Input: serialized, + Tool: variant.Name, + }) + if err != nil { + b.logger.Error(ctx, "failed to track non-injected tool usage", slog.Error(err)) + } + } + } } } @@ -836,6 +961,25 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } +func parseToolName(name string) (string, string, bool) { + serverName, toolName, found := strings.Cut(name, MCPProxyDelimiter) + return serverName, toolName, found +} + +func (b *Bridge) isInjectedTool(name string) bool { + serverName, toolName, found := parseToolName(name) + if !found { + return false + } + + mcp, ok := b.mcpBridges[serverName] + if !ok { + return false + } + + return mcp.HasTool(toolName) +} + func getAnthropicErrorResponse(err error) *AnthropicErrorResponse { var apierr *anthropic.Error if !errors.As(err, &apierr) { diff --git a/aibridged/mcp.go b/aibridged/mcp.go index 9b766463210b2..fac21a5f859b3 100644 --- a/aibridged/mcp.go +++ b/aibridged/mcp.go @@ -9,19 +9,20 @@ import ( "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/client/transport" "github.com/mark3labs/mcp-go/mcp" + "golang.org/x/exp/maps" "golang.org/x/xerrors" ) -type BridgeMCPProxy struct { +type MCPToolBridge struct { name string client *client.Client logger slog.Logger - foundTools []anthropic.BetaToolUnionParam + foundTools map[string]anthropic.BetaToolUnionParam } const MCPProxyDelimiter = "_" -func NewBridgeMCPProxy(name, serverURL string, headers map[string]string, logger slog.Logger) (*BridgeMCPProxy, error) { +func NewMCPToolBridge(name, serverURL string, headers map[string]string, logger slog.Logger) (*MCPToolBridge, error) { //ts := transport.NewMemoryTokenStore() //if err := ts.SaveToken(&transport.Token{ // AccessToken: token, @@ -38,14 +39,14 @@ func NewBridgeMCPProxy(name, serverURL string, headers map[string]string, logger return nil, xerrors.Errorf("create streamable http client: %w", err) } - return &BridgeMCPProxy{ + return &MCPToolBridge{ name: name, client: mcpClient, logger: logger, }, nil } -func (b *BridgeMCPProxy) Init(ctx context.Context) error { +func (b *MCPToolBridge) Init(ctx context.Context) error { if err := b.client.Start(ctx); err != nil { return xerrors.Errorf("start client: %w", err) } @@ -59,11 +60,18 @@ func (b *BridgeMCPProxy) Init(ctx context.Context) error { return nil } -func (b *BridgeMCPProxy) ListTools() []anthropic.BetaToolUnionParam { - return b.foundTools +func (b *MCPToolBridge) ListTools() []anthropic.BetaToolUnionParam { + return maps.Values(b.foundTools) } -func (b *BridgeMCPProxy) CallTool(ctx context.Context, name string, input any) (*mcp.CallToolResult, error) { +func (b *MCPToolBridge) HasTool(name string) bool { + if b.foundTools == nil { return false } + + _, ok := b.foundTools[name] + return ok +} + +func (b *MCPToolBridge) CallTool(ctx context.Context, name string, input any) (*mcp.CallToolResult, error) { return b.client.CallTool(ctx, mcp.CallToolRequest{ Params: mcp.CallToolParams{ Name: name, @@ -72,7 +80,7 @@ func (b *BridgeMCPProxy) CallTool(ctx context.Context, name string, input any) ( }) } -func (b *BridgeMCPProxy) fetchMCPTools(ctx context.Context) ([]anthropic.BetaToolUnionParam, error) { +func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]anthropic.BetaToolUnionParam, error) { initReq := mcp.InitializeRequest{ Params: mcp.InitializeParams{ ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, @@ -87,7 +95,7 @@ func (b *BridgeMCPProxy) fetchMCPTools(ctx context.Context) ([]anthropic.BetaToo if err != nil { return nil, xerrors.Errorf("init MCP client: %w", err) } - fmt.Println(result.ProtocolVersion) // TODO: remove. + fmt.Printf("mcp(%q)], %+v\n", result.ServerInfo.Name, result) // TODO: remove. // Test tool listing tools, err := b.client.ListTools(ctx, mcp.ListToolsRequest{}) @@ -95,9 +103,9 @@ func (b *BridgeMCPProxy) fetchMCPTools(ctx context.Context) ([]anthropic.BetaToo return nil, xerrors.Errorf("list MCP tools: %w", err) } - out := make([]anthropic.BetaToolUnionParam, 0, len(tools.Tools)) + out := make(map[string]anthropic.BetaToolUnionParam, len(tools.Tools)) for _, tool := range tools.Tools { - out = append(out, anthropic.BetaToolUnionParam{ + out[tool.Name] = anthropic.BetaToolUnionParam{ OfTool: &anthropic.BetaToolParam{ InputSchema: anthropic.BetaToolInputSchemaParam{ Properties: tool.InputSchema.Properties, @@ -107,12 +115,12 @@ func (b *BridgeMCPProxy) fetchMCPTools(ctx context.Context) ([]anthropic.BetaToo Description: anthropic.String(tool.Description), Type: anthropic.BetaToolTypeCustom, }, - }) + } } return out, nil } -func (b *BridgeMCPProxy) Close() { +func (b *MCPToolBridge) Close() { // TODO: atomically close. } diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index e253dc8e854b7..5dbc51dcf773b 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -323,18 +323,19 @@ func (*TrackUserPromptsResponse) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} } -type TrackToolUseRequest struct { +type TrackToolUsageRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Tool string `protobuf:"bytes,1,opt,name=tool,proto3" json:"tool,omitempty"` - Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` - Input map[string]string `protobuf:"bytes,3,rep,name=input,proto3" json:"input,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Tool string `protobuf:"bytes,1,opt,name=tool,proto3" json:"tool,omitempty"` + Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` + Input map[string]string `protobuf:"bytes,3,rep,name=input,proto3" json:"input,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Injected bool `protobuf:"varint,4,opt,name=injected,proto3" json:"injected,omitempty"` } -func (x *TrackToolUseRequest) Reset() { - *x = TrackToolUseRequest{} +func (x *TrackToolUsageRequest) Reset() { + *x = TrackToolUsageRequest{} if protoimpl.UnsafeEnabled { mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -342,13 +343,13 @@ func (x *TrackToolUseRequest) Reset() { } } -func (x *TrackToolUseRequest) String() string { +func (x *TrackToolUsageRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TrackToolUseRequest) ProtoMessage() {} +func (*TrackToolUsageRequest) ProtoMessage() {} -func (x *TrackToolUseRequest) ProtoReflect() protoreflect.Message { +func (x *TrackToolUsageRequest) ProtoReflect() protoreflect.Message { mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -360,40 +361,47 @@ func (x *TrackToolUseRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TrackToolUseRequest.ProtoReflect.Descriptor instead. -func (*TrackToolUseRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use TrackToolUsageRequest.ProtoReflect.Descriptor instead. +func (*TrackToolUsageRequest) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{6} } -func (x *TrackToolUseRequest) GetTool() string { +func (x *TrackToolUsageRequest) GetTool() string { if x != nil { return x.Tool } return "" } -func (x *TrackToolUseRequest) GetModel() string { +func (x *TrackToolUsageRequest) GetModel() string { if x != nil { return x.Model } return "" } -func (x *TrackToolUseRequest) GetInput() map[string]string { +func (x *TrackToolUsageRequest) GetInput() map[string]string { if x != nil { return x.Input } return nil } -type TrackToolUseResponse struct { +func (x *TrackToolUsageRequest) GetInjected() bool { + if x != nil { + return x.Injected + } + return false +} + +type TrackToolUsageResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } -func (x *TrackToolUseResponse) Reset() { - *x = TrackToolUseResponse{} +func (x *TrackToolUsageResponse) Reset() { + *x = TrackToolUsageResponse{} if protoimpl.UnsafeEnabled { mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -401,13 +409,13 @@ func (x *TrackToolUseResponse) Reset() { } } -func (x *TrackToolUseResponse) String() string { +func (x *TrackToolUsageResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TrackToolUseResponse) ProtoMessage() {} +func (*TrackToolUsageResponse) ProtoMessage() {} -func (x *TrackToolUseResponse) ProtoReflect() protoreflect.Message { +func (x *TrackToolUsageResponse) ProtoReflect() protoreflect.Message { mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -419,8 +427,8 @@ func (x *TrackToolUseResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TrackToolUseResponse.ProtoReflect.Descriptor instead. -func (*TrackToolUseResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use TrackToolUsageResponse.ProtoReflect.Descriptor instead. +func (*TrackToolUsageResponse) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{7} } @@ -460,46 +468,49 @@ var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x1a, 0x0a, 0x18, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, - 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xba, 0x01, 0x0a, 0x13, - 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x3f, 0x0a, - 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, - 0x6f, 0x6c, 0x55, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6e, 0x70, - 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x1a, 0x38, + 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xda, 0x01, 0x0a, 0x15, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, + 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, + 0x41, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, + 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, + 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x16, 0x0a, 0x14, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x32, 0xe6, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, - 0x70, 0x74, 0x12, 0x1d, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, - 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x41, 0x75, - 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, - 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, - 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x54, - 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x12, - 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, - 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, - 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, - 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x32, 0xec, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, + 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1d, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, + 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, + 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, + 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, + 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, + 0x10, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, + 0x73, 0x12, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, + 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, + 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, + 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, + 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, + 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, + 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -522,22 +533,22 @@ var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*TrackTokenUsageResponse)(nil), // 3: aibridged.TrackTokenUsageResponse (*TrackUserPromptsRequest)(nil), // 4: aibridged.TrackUserPromptsRequest (*TrackUserPromptsResponse)(nil), // 5: aibridged.TrackUserPromptsResponse - (*TrackToolUseRequest)(nil), // 6: aibridged.TrackToolUseRequest - (*TrackToolUseResponse)(nil), // 7: aibridged.TrackToolUseResponse + (*TrackToolUsageRequest)(nil), // 6: aibridged.TrackToolUsageRequest + (*TrackToolUsageResponse)(nil), // 7: aibridged.TrackToolUsageResponse nil, // 8: aibridged.TrackTokenUsageRequest.OtherEntry - nil, // 9: aibridged.TrackToolUseRequest.InputEntry + nil, // 9: aibridged.TrackToolUsageRequest.InputEntry } var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ 8, // 0: aibridged.TrackTokenUsageRequest.other:type_name -> aibridged.TrackTokenUsageRequest.OtherEntry - 9, // 1: aibridged.TrackToolUseRequest.input:type_name -> aibridged.TrackToolUseRequest.InputEntry + 9, // 1: aibridged.TrackToolUsageRequest.input:type_name -> aibridged.TrackToolUsageRequest.InputEntry 0, // 2: aibridged.AIBridgeDaemon.AuditPrompt:input_type -> aibridged.AuditPromptRequest 2, // 3: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest 4, // 4: aibridged.AIBridgeDaemon.TrackUserPrompts:input_type -> aibridged.TrackUserPromptsRequest - 6, // 5: aibridged.AIBridgeDaemon.TrackToolUse:input_type -> aibridged.TrackToolUseRequest + 6, // 5: aibridged.AIBridgeDaemon.TrackToolUsage:input_type -> aibridged.TrackToolUsageRequest 1, // 6: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse 3, // 7: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse 5, // 8: aibridged.AIBridgeDaemon.TrackUserPrompts:output_type -> aibridged.TrackUserPromptsResponse - 7, // 9: aibridged.AIBridgeDaemon.TrackToolUse:output_type -> aibridged.TrackToolUseResponse + 7, // 9: aibridged.AIBridgeDaemon.TrackToolUsage:output_type -> aibridged.TrackToolUsageResponse 6, // [6:10] is the sub-list for method output_type 2, // [2:6] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name @@ -624,7 +635,7 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackToolUseRequest); i { + switch v := v.(*TrackToolUsageRequest); i { case 0: return &v.state case 1: @@ -636,7 +647,7 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackToolUseResponse); i { + switch v := v.(*TrackToolUsageResponse); i { case 0: return &v.state case 1: diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index b70a1d0dba52e..572b5473765c7 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -25,12 +25,13 @@ message TrackUserPromptsRequest { } message TrackUserPromptsResponse {} -message TrackToolUseRequest { +message TrackToolUsageRequest { string tool = 1; string model = 2; map input = 3; + bool injected = 4; } -message TrackToolUseResponse {} +message TrackToolUsageResponse {} // AIBridgeDaemon describes the service exposed by coderd, which allows the AI bridge to send data to coderd. service AIBridgeDaemon { @@ -40,5 +41,5 @@ service AIBridgeDaemon { rpc AuditPrompt(AuditPromptRequest) returns (AuditPromptResponse); rpc TrackTokenUsage(TrackTokenUsageRequest) returns (TrackTokenUsageResponse); rpc TrackUserPrompts(TrackUserPromptsRequest) returns (TrackUserPromptsResponse); - rpc TrackToolUse(TrackToolUseRequest) returns (TrackToolUseResponse); + rpc TrackToolUsage(TrackToolUsageRequest) returns (TrackToolUsageResponse); } diff --git a/aibridged/proto/aibridged_drpc.pb.go b/aibridged/proto/aibridged_drpc.pb.go index b2421c013769a..33efc227104f9 100644 --- a/aibridged/proto/aibridged_drpc.pb.go +++ b/aibridged/proto/aibridged_drpc.pb.go @@ -41,7 +41,7 @@ type DRPCAIBridgeDaemonClient interface { AuditPrompt(ctx context.Context, in *AuditPromptRequest) (*AuditPromptResponse, error) TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) TrackUserPrompts(ctx context.Context, in *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) - TrackToolUse(ctx context.Context, in *TrackToolUseRequest) (*TrackToolUseResponse, error) + TrackToolUsage(ctx context.Context, in *TrackToolUsageRequest) (*TrackToolUsageResponse, error) } type drpcAIBridgeDaemonClient struct { @@ -81,9 +81,9 @@ func (c *drpcAIBridgeDaemonClient) TrackUserPrompts(ctx context.Context, in *Tra return out, nil } -func (c *drpcAIBridgeDaemonClient) TrackToolUse(ctx context.Context, in *TrackToolUseRequest) (*TrackToolUseResponse, error) { - out := new(TrackToolUseResponse) - err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackToolUse", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) +func (c *drpcAIBridgeDaemonClient) TrackToolUsage(ctx context.Context, in *TrackToolUsageRequest) (*TrackToolUsageResponse, error) { + out := new(TrackToolUsageResponse) + err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackToolUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -94,7 +94,7 @@ type DRPCAIBridgeDaemonServer interface { AuditPrompt(context.Context, *AuditPromptRequest) (*AuditPromptResponse, error) TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) TrackUserPrompts(context.Context, *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) - TrackToolUse(context.Context, *TrackToolUseRequest) (*TrackToolUseResponse, error) + TrackToolUsage(context.Context, *TrackToolUsageRequest) (*TrackToolUsageResponse, error) } type DRPCAIBridgeDaemonUnimplementedServer struct{} @@ -111,7 +111,7 @@ func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackUserPrompts(context.Context return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } -func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackToolUse(context.Context, *TrackToolUseRequest) (*TrackToolUseResponse, error) { +func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackToolUsage(context.Context, *TrackToolUsageRequest) (*TrackToolUsageResponse, error) { return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } @@ -149,14 +149,14 @@ func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc. ) }, DRPCAIBridgeDaemonServer.TrackUserPrompts, true case 3: - return "/aibridged.AIBridgeDaemon/TrackToolUse", drpcEncoding_File_aibridged_proto_aibridged_proto{}, + return "/aibridged.AIBridgeDaemon/TrackToolUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAIBridgeDaemonServer). - TrackToolUse( + TrackToolUsage( ctx, - in1.(*TrackToolUseRequest), + in1.(*TrackToolUsageRequest), ) - }, DRPCAIBridgeDaemonServer.TrackToolUse, true + }, DRPCAIBridgeDaemonServer.TrackToolUsage, true default: return "", nil, nil, nil, false } @@ -214,16 +214,16 @@ func (x *drpcAIBridgeDaemon_TrackUserPromptsStream) SendAndClose(m *TrackUserPro return x.CloseSend() } -type DRPCAIBridgeDaemon_TrackToolUseStream interface { +type DRPCAIBridgeDaemon_TrackToolUsageStream interface { drpc.Stream - SendAndClose(*TrackToolUseResponse) error + SendAndClose(*TrackToolUsageResponse) error } -type drpcAIBridgeDaemon_TrackToolUseStream struct { +type drpcAIBridgeDaemon_TrackToolUsageStream struct { drpc.Stream } -func (x *drpcAIBridgeDaemon_TrackToolUseStream) SendAndClose(m *TrackToolUseResponse) error { +func (x *drpcAIBridgeDaemon_TrackToolUsageStream) SendAndClose(m *TrackToolUsageResponse) error { if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { return err } diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index 79c440a70ca0b..0f571bbc49622 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -50,7 +50,7 @@ func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPrompt return &proto.TrackUserPromptsResponse{}, nil } -func (s *Server) TrackToolUse(ctx context.Context, in *proto.TrackToolUseRequest) (*proto.TrackToolUseResponse, error) { +func (s *Server) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageRequest) (*proto.TrackToolUsageResponse, error) { raw, err := json.Marshal(in) if err != nil { return nil, xerrors.Errorf("marshal event: %w", err) @@ -61,7 +61,7 @@ func (s *Server) TrackToolUse(ctx context.Context, in *proto.TrackToolUseRequest return nil, xerrors.Errorf("store event: %w", err) } - return &proto.TrackToolUseResponse{}, nil + return &proto.TrackToolUsageResponse{}, nil } func NewServer(lifecycleCtx context.Context, store database.Store) (*Server, error) { From a2405584a09fc90a38e0aa5cdc9a85b166fe2e46 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 14 Jul 2025 10:40:40 +0200 Subject: [PATCH 30/61] demo prep Signed-off-by: Danny Kopping --- aibridged/bridge.go | 150 +++++++++++++++++++++++++------------------- coderd/aibridge.go | 9 +++ 2 files changed, 96 insertions(+), 63 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 5df8f80e18c20..e6966c3e9a313 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -512,6 +512,44 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { return } + // Policy examples. + if strings.Contains(string(in.Model), "opus") { + err := xerrors.Errorf("%q model is not allowed", in.Model) + b.setError(err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + for _, m := range in.Messages { + for _, c := range m.Content { + if c.OfText == nil { + continue + } + + if strings.Contains(c.OfText.Text, ".env") { + http.Error(w, "Request blocked due to attempted access to sensitive file; this has been logged.", http.StatusBadRequest) + return + } + } + } + + for _, t := range in.Tools { + if t.OfTool == nil { + continue + } + + if strings.Contains(t.OfTool.Name, "mcp__") && (!strings.Contains(t.OfTool.Name, "go") && !strings.Contains(t.OfTool.Name, "typescript")) { + segs := strings.Split(t.OfTool.Name, "__") + var serverName string + if len(segs) >= 1 { + serverName = segs[1] + } + + http.Error(w, fmt.Sprintf("Request blocked due to MCP server %q being used; this has been logged.", serverName), http.StatusBadRequest) + return + } + } + in.Tools = append(in.Tools, anthropic.BetaToolUnionParam{OfTool: &anthropic.BetaToolParam{ Name: "get_coordinates", Description: anthropic.String("Accepts a place as an address, then returns the latitude and longitude coordinates."), @@ -555,45 +593,12 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") client := anthropic.NewClient(opts...) - if !in.UseStreaming() || isSmallFastModel { - msg, err := client.Beta.Messages.New(ctx, messages) - if err != nil { - if antErr := getAnthropicErrorResponse(err); antErr != nil { - b.setError(antErr) - http.Error(w, antErr.Error.Message, antErr.StatusCode) - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - b.setError(err) - } - return - } - - // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! - _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ - MsgId: msg.ID, - Model: string(msg.Model), - InputTokens: msg.Usage.InputTokens, - OutputTokens: msg.Usage.OutputTokens, - Other: map[string]int64{ - "web_search_requests": msg.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": msg.Usage.CacheCreationInputTokens, - "cache_read_input": msg.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": msg.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": msg.Usage.CacheCreation.Ephemeral5mInputTokens, - }, - }) - if err != nil { - b.logger.Error(ctx, "failed to track usage", slog.Error(err)) - } - - out := []byte(msg.RawJSON()) - w.WriteHeader(http.StatusOK) - _, _ = w.Write(out) + if !in.UseStreaming() { + http.Error(w, "streaming API supported only", http.StatusBadRequest) return } streamCtx, streamCancel := context.WithCancelCause(r.Context()) - defer streamCancel(xerrors.New("deferred")) es := newEventStream(anthropicEventStream) @@ -678,31 +683,41 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { continue } case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): - // Don't relay message_delta events which indicate tool use. - // if event.AsMessageDelta().Delta.StopReason == anthropic.BetaStopReasonToolUse { + delta := event.AsMessageDelta() + if delta.Delta.StopReason == anthropic.BetaStopReasonEndTurn { + // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! + if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ + MsgId: message.ID, + Model: string(message.Model), + InputTokens: delta.Usage.InputTokens, + OutputTokens: delta.Usage.OutputTokens, + Other: map[string]int64{ + "web_search_requests": delta.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": delta.Usage.CacheCreationInputTokens, + "cache_read_input": delta.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, + }, + }); err != nil { + b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) + } + } + + // Don't relay message_delta events which indicate injected tool use. if len(pendingToolCalls) > 0 && b.isInjectedTool(lastToolName) { continue } - //} + + // If currently calling a tool. + if message.Content[len(message.Content)-1].Type == string(ant_constant.ValueOf[ant_constant.ToolUse]()) { + toolName := message.Content[len(message.Content)-1].AsToolUse().Name + if len(pendingToolCalls) > 0 && b.isInjectedTool(toolName) { + continue + } + } // Don't send message_stop until all tools have been called. case string(ant_constant.ValueOf[ant_constant.MessageStop]()): - // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! - if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ - MsgId: message.ID, - Model: string(message.Model), - InputTokens: message.Usage.InputTokens, - OutputTokens: message.Usage.OutputTokens, - Other: map[string]int64{ - "web_search_requests": message.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": message.Usage.CacheCreationInputTokens, - "cache_read_input": message.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, - }, - }); err != nil { - b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) - } if len(pendingToolCalls) > 0 { // Append the whole message from this stream as context since we'll be sending a new request with the tool results. @@ -746,9 +761,9 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { fmt.Printf("[event] %s\n[tool(%q)] %s %+v\n\n", event.RawJSON(), id, name, input) _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ - Model: string(message.Model), - Input: serialized, - Tool: toolName, + Model: string(message.Model), + Input: serialized, + Tool: toolName, Injected: true, }) if err != nil { @@ -897,9 +912,9 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(&buf).Encode(variant.Input) _ = json.NewDecoder(&buf).Decode(&serialized) _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ - Model: string(message.Model), - Input: serialized, - Tool: variant.Name, + Model: string(message.Model), + Input: serialized, + Tool: variant.Name, }) if err != nil { b.logger.Error(ctx, "failed to track non-injected tool usage", slog.Error(err)) @@ -924,16 +939,15 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } b.logger.Error(ctx, "anthropic stream error", slog.Error(streamErr)) - b.setError(streamErr) if antErr := getAnthropicErrorResponse(streamErr); antErr != nil { err = es.TrySend(streamCtx, antErr, "") if err != nil { b.logger.Error(ctx, "failed to send error", slog.Error(err)) } - http.Error(w, antErr.Error.Message, ProxyErrCode) + b.setError(antErr) } else { - http.Error(w, streamErr.Error(), ProxyErrCode) + b.setError(streamErr) } } @@ -957,6 +971,16 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { case <-streamCtx.Done(): } + // Close the underlying connection by hijacking it + if hijacker, ok := w.(http.Hijacker); ok { + conn, _, err := hijacker.Hijack() + if err != nil { + b.logger.Error(ctx, "failed to hijack connection", slog.Error(err)) + } else { + conn.Close() // This closes the TCP connection entirely + } + } + break } } diff --git a/coderd/aibridge.go b/coderd/aibridge.go index 5de75eacc77f4..0ac72a0c8a66a 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "time" "cdr.dev/slog" @@ -19,6 +20,11 @@ type rt struct { } func (r *rt) RoundTrip(req *http.Request) (*http.Response, error) { + start := time.Now() + defer func() { + fmt.Printf("req to %q started %v completed\n", req.URL.String(), start.Local().Format(time.RFC3339Nano)) + }() + resp, err := r.RoundTripper.RoundTrip(req) if err != nil || resp.StatusCode == aibridged.ProxyErrCode { @@ -56,12 +62,15 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { if err != nil { api.Logger.Error(ctx, "failed to parse bridge address", slog.Error(err)) http.Error(rw, "failed to parse bridge address", http.StatusInternalServerError) + return } rp := httputil.NewSingleHostReverseProxy(u) rp.Transport = &rt{RoundTripper: http.DefaultTransport, server: server} rp.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { api.Logger.Error(ctx, "aibridge reverse proxy error", slog.Error(err)) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return } http.StripPrefix("/api/v2/aibridge", rp).ServeHTTP(rw, r) } From 236b026b9d9ad15e29f26a0642e7e0ddfb57cc40 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 16 Jul 2025 13:48:17 +0200 Subject: [PATCH 31/61] start working on tests Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 19 +- aibridged/anthropic.go | 1 - aibridged/bridge.go | 241 +++++------ aibridged/bridge_test.go | 275 ++++++++---- .../anthropic/single_builtin_tool.txtar | 45 ++ aibridged/mcp.go | 17 +- aibridged/proto/aibridged.pb.go | 380 ++++++----------- aibridged/proto/aibridged.proto | 17 +- aibridged/proto/aibridged_drpc.pb.go | 74 +--- aibridged/proxy.go | 391 ------------------ aibridged/sse_parser.go | 124 ++++++ aibridged/streaming.go | 6 +- aibridged/util/zero_marshaler.go | 8 +- coderd/aibridgedserver/aibridgedserver.go | 8 +- ...aibridge.up.sql => 000350_aibridge.up.sql} | 0 codersdk/deployment.go | 30 +- go.mod | 4 - go.sum | 8 - site/src/api/typesGenerated.ts | 8 +- 19 files changed, 671 insertions(+), 985 deletions(-) create mode 100644 aibridged/fixtures/anthropic/single_builtin_tool.txtar delete mode 100644 aibridged/proxy.go create mode 100644 aibridged/sse_parser.go rename coderd/database/migrations/{000349_aibridge.up.sql => 000350_aibridge.up.sql} (100%) diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index b120c05bc8406..47f3d9e76d19a 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -82,7 +82,10 @@ func New(rpcDialer Dialer, httpAddr string, logger slog.Logger, bridgeCfg coders daemon.wg.Add(1) go daemon.connect() + + daemon.wg.Add(1) go func() { + defer daemon.wg.Done() err := bridge.Serve() // TODO: better error handling. // TODO: close on shutdown. @@ -164,16 +167,6 @@ func (s *Server) client() (proto.DRPCAIBridgeDaemonClient, bool) { } } -func (s *Server) AuditPrompt(ctx context.Context, in *proto.AuditPromptRequest) (*proto.AuditPromptResponse, error) { - out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.AuditPromptResponse, error) { - return client.AuditPrompt(ctx, in) - }) - if err != nil { - return nil, err - } - return out, nil -} - func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackTokenUsageResponse, error) { return client.TrackTokenUsage(ctx, in) @@ -184,9 +177,9 @@ func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageR return out, nil } -func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPromptsRequest) (*proto.TrackUserPromptsResponse, error) { - out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackUserPromptsResponse, error) { - return client.TrackUserPrompts(ctx, in) +func (s *Server) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptRequest) (*proto.TrackUserPromptResponse, error) { + out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackUserPromptResponse, error) { + return client.TrackUserPrompt(ctx, in) }) if err != nil { return nil, err diff --git a/aibridged/anthropic.go b/aibridged/anthropic.go index db3a30dad9f64..fb24b07b0074e 100644 --- a/aibridged/anthropic.go +++ b/aibridged/anthropic.go @@ -22,7 +22,6 @@ func ConvertStringContentToArrayTest(raw []byte) ([]byte, error) { // // Each input message content may be either a single string or an array of content blocks, where each block has a // specific type. Using a string for content is shorthand for an array of one content block of type "text". -// func convertStringContentToArray(raw []byte) ([]byte, error) { in := gjson.ParseBytes(raw) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index e6966c3e9a313..197d3283f6d85 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -16,7 +16,6 @@ import ( "sync" "time" - "cdr.dev/slog" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" @@ -24,10 +23,9 @@ import ( "github.com/mark3labs/mcp-go/mcp" "github.com/openai/openai-go" "github.com/openai/openai-go/shared/constant" - "golang.org/x/sync/errgroup" "golang.org/x/xerrors" - "github.com/invopop/jsonschema" + "cdr.dev/slog" "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/codersdk" @@ -90,47 +88,53 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli bridge.clientFn = clientFn bridge.logger = logger - time.Sleep(time.Second * 3) - - const ( - githubMCPName = "github" - coderMCPName = "coder" - ) - githubMCP, err := NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ - "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), - }, logger.Named("mcp-bridge-github")) - if err != nil { - return nil, xerrors.Errorf("github MCP bridge setup: %w", err) - } - coderMCP, err := NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ - "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), - // This is necessary to even access the MCP endpoint. - "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), - }, logger.Named("mcp-bridge-coder")) - if err != nil { - return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) - } - - bridge.mcpBridges = map[string]*MCPToolBridge{ - githubMCPName: githubMCP, - coderMCPName: coderMCP, - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - var eg errgroup.Group - eg.Go(func() error { - return githubMCP.Init(ctx) - }) - eg.Go(func() error { - return coderMCP.Init(ctx) - }) - - // This must block requests until MCP proxies are setup. - if err := eg.Wait(); err != nil { - return nil, xerrors.Errorf("MCP proxy init: %w", err) - } + // const ( + // githubMCPName = "github" + // coderMCPName = "coder" + // ) + // githubMCP, err := NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ + // "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), + // }, logger.Named("mcp-bridge-github")) + // if err != nil { + // return nil, xerrors.Errorf("github MCP bridge setup: %w", err) + // } + // coderMCP, err := NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ + // "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), + // // This is necessary to even access the MCP endpoint. + // "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), + // }, logger.Named("mcp-bridge-coder")) + // if err != nil { + // return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) + // } + + // bridge.mcpBridges = map[string]*MCPToolBridge{ + // githubMCPName: githubMCP, + // coderMCPName: coderMCP, + // } + + // ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + // defer cancel() + + // var eg errgroup.Group + // eg.Go(func() error { + // err := githubMCP.Init(ctx) + // if err == nil { + // return nil + // } + // return xerrors.Errorf("github: %w", err) + // }) + // eg.Go(func() error { + // err := coderMCP.Init(ctx) + // if err == nil { + // return nil + // } + // return xerrors.Errorf("coder: %w", err) + // }) + + // // This must block requests until MCP proxies are setup. + // if err := eg.Wait(); err != nil { + // return nil, xerrors.Errorf("MCP proxy init: %w", err) + // } return &bridge, nil } @@ -199,7 +203,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { prompt, err := in.LastUserPrompt() // TODO: error handling. if prompt != nil { - if _, err = coderdClient.TrackUserPrompts(ctx, &proto.TrackUserPromptsRequest{ + if _, err = coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ Model: in.Model, Prompt: *prompt, }); err != nil { @@ -419,9 +423,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { streamCancel(xerrors.New("gracefully done")) - select { - case <-streamCtx.Done(): - } + <-streamCtx.Done() } else { completion, err := client.Chat.Completions.New(ctx, in.ChatCompletionNewParams) if err != nil { @@ -502,7 +504,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // var in streamer // if useBeta { var in BetaMessageNewParamsWrapper - //} else { + // } else { // in = &MessageNewParamsWrapper{} //} @@ -550,12 +552,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } - in.Tools = append(in.Tools, anthropic.BetaToolUnionParam{OfTool: &anthropic.BetaToolParam{ - Name: "get_coordinates", - Description: anthropic.String("Accepts a place as an address, then returns the latitude and longitude coordinates."), - InputSchema: GetCoordinatesInputSchema, - }}) - for _, proxy := range b.mcpBridges { in.Tools = append(in.Tools, proxy.ListTools()...) } @@ -567,7 +563,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if !isSmallFastModel { prompt, err := in.LastUserPrompt() // TODO: error handling. if prompt != nil { - if _, err = coderdClient.TrackUserPrompts(ctx, &proto.TrackUserPromptsRequest{ + if _, err = coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ Prompt: *prompt, Model: string(in.Model), }); err != nil { @@ -590,6 +586,13 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if reqBetaHeader := r.Header.Get("anthropic-beta"); strings.TrimSpace(reqBetaHeader) != "" { opts = append(opts, option.WithHeader("anthropic-beta", reqBetaHeader)) } + opts = append(opts, option.WithMiddleware(LoggingMiddleware)) + + // Lib will automatically look for ANTHROPIC_API_KEY env. + if _, ok := os.LookupEnv("ANTHROPIC_API_KEY"); !ok { + opts = append(opts, option.WithAPIKey(b.cfg.Anthropic.Key.String())) + } + opts = append(opts, option.WithBaseURL(b.cfg.Anthropic.BaseURL.String())) // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") client := anthropic.NewClient(opts...) @@ -602,11 +605,11 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { es := newEventStream(anthropicEventStream) - //var buf strings.Builder - //in.Messages[0].Content = []anthropic.BetaContentBlockParamUnion{in.Messages[0].Content[len(in.Messages[0].Content) - 1]} + // var buf strings.Builder + // in.Messages[0].Content = []anthropic.BetaContentBlockParamUnion{in.Messages[0].Content[len(in.Messages[0].Content) - 1]} // - //json.NewEncoder(&buf).Encode(in) - //fmt.Println(strings.Replace(buf.String(), "'", "\\'", -1)) + // json.NewEncoder(&buf).Encode(in) + // fmt.Println(strings.Replace(buf.String(), "'", "\\'", -1)) var wg sync.WaitGroup wg.Add(1) @@ -676,6 +679,25 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { continue } case string(ant_constant.ValueOf[ant_constant.MessageStart]()): + // Anthropic's docs only mention usage in message_delta events, but it's also present in message_start. + // See https://docs.anthropic.com/en/docs/build-with-claude/streaming#event-types. + start := event.AsMessageStart() + if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ + MsgId: message.ID, + Model: string(message.Model), + InputTokens: start.Message.Usage.InputTokens, + OutputTokens: start.Message.Usage.OutputTokens, + Other: map[string]int64{ + "web_search_requests": start.Message.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": start.Message.Usage.CacheCreationInputTokens, + "cache_read_input": start.Message.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, + }, + }); err != nil { + b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) + } + if !isFirst { // Don't send message_start unless first message! // We're sending multiple messages back and forth with the API, but from the client's perspective @@ -684,23 +706,20 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): delta := event.AsMessageDelta() - if delta.Delta.StopReason == anthropic.BetaStopReasonEndTurn { - // TODO: these figures don't seem to exactly match what Claude Code reports. Find the source of inconsistency! - if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ - MsgId: message.ID, - Model: string(message.Model), - InputTokens: delta.Usage.InputTokens, - OutputTokens: delta.Usage.OutputTokens, - Other: map[string]int64{ - "web_search_requests": delta.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": delta.Usage.CacheCreationInputTokens, - "cache_read_input": delta.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, - }, - }); err != nil { - b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) - } + if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ + MsgId: message.ID, + Model: string(message.Model), + InputTokens: delta.Usage.InputTokens, + OutputTokens: delta.Usage.OutputTokens, + Other: map[string]int64{ + "web_search_requests": delta.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": delta.Usage.CacheCreationInputTokens, + "cache_read_input": delta.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, + }, + }); err != nil { + b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) } // Don't relay message_delta events which indicate injected tool use. @@ -802,7 +821,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { switch cb := content.(type) { case mcp.TextContent: - //messages.Messages = append(messages.Messages, + // messages.Messages = append(messages.Messages, // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, cb.Text, false)), //) toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ @@ -819,7 +838,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // For text resources, include the text content val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", resource.MIMEType, resource.URI, resource.Text) - //messages.Messages = append(messages.Messages, + // messages.Messages = append(messages.Messages, // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, val, false)), //) @@ -833,7 +852,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // For blob resources, include the base64 data with MIME type info val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", resource.MIMEType, resource.URI, resource.Blob) - //messages.Messages = append(messages.Messages, + // messages.Messages = append(messages.Messages, // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, val, false)), //) @@ -845,7 +864,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { hasValidResult = true default: b.logger.Error(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource))) - //messages.Messages = append(messages.Messages, + // messages.Messages = append(messages.Messages, // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, "Error: unknown embedded resource type", true)), //) @@ -860,7 +879,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { default: // Not supported - but we must still provide a tool_result to match the tool_use b.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb)), slog.F("json", out.String())) - //messages.Messages = append(messages.Messages, + // messages.Messages = append(messages.Messages, // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, "Error: unsupported tool result type", true)), //) @@ -876,7 +895,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // If no content was processed, still add a tool_result if !hasValidResult { - //messages.Messages = append(messages.Messages, + // messages.Messages = append(messages.Messages, // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, "Error: no valid tool result content", true)), //) @@ -967,19 +986,19 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { streamCancel(xerrors.New("gracefully done")) } - select { - case <-streamCtx.Done(): - } + <-streamCtx.Done() - // Close the underlying connection by hijacking it - if hijacker, ok := w.(http.Hijacker); ok { - conn, _, err := hijacker.Hijack() - if err != nil { - b.logger.Error(ctx, "failed to hijack connection", slog.Error(err)) - } else { - conn.Close() // This closes the TCP connection entirely - } - } + // TODO: do we need to do this? + // // Close the underlying connection by hijacking it + // if hijacker, ok := w.(http.Hijacker); ok { + // conn, _, err := hijacker.Hijack() + // if err != nil { + // b.logger.Error(ctx, "failed to hijack connection", slog.Error(err)) + // } else { + // conn.Close() // This closes the TCP connection entirely + // b.logger.Debug(ctx, "connection closed, stream over") + // } + // } break } @@ -1038,38 +1057,6 @@ type AnthropicErrorResponse struct { StatusCode int `json:"-"` } -type GetCoordinatesInput struct { - Location string `json:"location" jsonschema_description:"The location to look up."` -} - -var GetCoordinatesInputSchema = GenerateSchema[GetCoordinatesInput]() - -type GetCoordinateResponse struct { - Long float64 `json:"long"` - Lat float64 `json:"lat"` -} - -func GetCoordinates(location string) GetCoordinateResponse { - return GetCoordinateResponse{ - Long: -122.4194, - Lat: 37.7749, - } -} - -func GenerateSchema[T any]() anthropic.BetaToolInputSchemaParam { - reflector := jsonschema.Reflector{ - AllowAdditionalProperties: false, - DoNotReference: true, - } - var v T - - schema := reflector.Reflect(v) - - return anthropic.BetaToolInputSchemaParam{ - Properties: schema.Properties, - } -} - func (b *Bridge) Serve() error { list, err := net.Listen("tcp", b.httpSrv.Addr) if err != nil { diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index 1cba4738b3151..240ec942cfcf9 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -1,19 +1,23 @@ package aibridged_test import ( + "bufio" + "bytes" "context" + _ "embed" "fmt" + "net" "net/http" "net/http/httptest" "strings" + "sync" "testing" - "time" - "github.com/stretchr/testify/require" - "golang.org/x/xerrors" + "golang.org/x/tools/txtar" "storj.io/drpc" - "github.com/coder/serpent" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/coder/coder/v2/aibridged" "github.com/coder/coder/v2/aibridged/proto" @@ -21,101 +25,204 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/testutil" + "github.com/coder/serpent" ) -func TestOpenAIStreaming(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - subpath := strings.TrimPrefix(r.URL.Path, "/api/v2/aibridge") - - switch subpath { - case "/v1/chat/completions": - // Set SSE headers - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - - w.WriteHeader(http.StatusOK) - - events := [][]byte{ - []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_SWduKE0DLAgLShfQqkQzwqq2","type":"function","function":{"name":"get_weather","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}`), - []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), - []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"location"}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), - []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), - []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Paris"}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), - []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null}`), - []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null}`), - []byte(`{"id":"chatcmpl-Bh6ikAZkQqJaGnddPnWSJmmZkZzT6","object":"chat.completion.chunk","created":1749613638,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":53,"completion_tokens":14,"total_tokens":67,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}}`), - []byte(`[DONE]`), - } - - // Get the flusher for streaming - flusher, ok := w.(http.Flusher) - if !ok { - t.Fatal("streaming unsupported") - } - - for _, event := range events { - _, err := w.Write([]byte(fmt.Sprintf("data: %s\n\n", event))) - require.NoError(t, err) - - // Flush immediately to send data - flusher.Flush() - - time.Sleep(testutil.IntervalFast) // Shorter delay for testing - } - } - })) - defer ts.Close() +var ( + //go:embed fixtures/anthropic/single_builtin_tool.txtar + antSingleBuiltinTool []byte +) - client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ - DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) { - values.AI.BridgeConfig.OpenAIBaseURL = serpent.String(ts.URL) - }), +func TestAnthropicMessagesStreaming(t *testing.T) { + t.Parallel() + + sessionToken := getSessionToken(t) + + t.Run("single builtin tool", func(t *testing.T) { + t.Parallel() + + arc := txtar.Parse(antSingleBuiltinTool) + t.Logf("%s: %s", t.Name(), arc.Comment) + require.Len(t, arc.Files, 2) + reqBody, respBody := arc.Files[0], arc.Files[1] + + ctx := testutil.Context(t, testutil.WaitLong) + srv := newFakeSSEServer(ctx, respBody.Data) + t.Cleanup(srv.Close) + + coderdClient := &fakeBridgeDaemonClient{} + + logger := testutil.Logger(t) + b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + Anthropic: codersdk.AIBridgeAnthropicConfig{ + BaseURL: serpent.String(srv.URL), + Key: serpent.String(sessionToken), + }, + }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return coderdClient, true + }) + require.NoError(t, err) + + go func() { + assert.NoError(t, b.Serve()) + }() + // Wait for bridge to come up. + require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) + + // Make API call to aibridge for Anthropic /v1/messages + req := createAnthropicMessagesReq(t, "http://"+b.Addr(), string(reqBody.Data)) + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + sp := aibridged.NewSSEParser() + require.NoError(t, sp.Parse(resp.Body)) + + assert.Contains(t, sp.AllEvents(), "message_start") + assert.Contains(t, sp.AllEvents(), "message_stop") + + require.Len(t, coderdClient.tokenUsages, 2) // One from message_start, one from message_delta. + assert.NotZero(t, calculateTotalInputTokens(coderdClient.tokenUsages)) + assert.NotZero(t, calculateTotalOutputTokens(coderdClient.tokenUsages)) + + require.Len(t, coderdClient.toolUsages, 1) + assert.Equal(t, "Read", coderdClient.toolUsages[0].Tool) + require.Contains(t, coderdClient.toolUsages[0].Input, "file_path") + assert.Equal(t, "/tmp/blah/foo", coderdClient.toolUsages[0].Input["file_path"]) + + require.Len(t, coderdClient.userPrompts, 1) + assert.Equal(t, "read the foo file", coderdClient.userPrompts[0].Prompt) }) - log := testutil.Logger(t) +} + +func calculateTotalOutputTokens(in []*proto.TrackTokenUsageRequest) int64 { + var total int64 + for _, el := range in { + total += el.InputTokens + } + return total +} - fakeDialer := func(ctx context.Context) (proto.DRPCAIBridgeDaemonClient, error) { - return &fakeBridgeDaemonClient{}, nil +func calculateTotalInputTokens(in []*proto.TrackTokenUsageRequest) int64 { + var total int64 + for _, el := range in { + total += el.OutputTokens } - bridgeSrv, err := aibridged.New(fakeDialer, "127.0.0.1:0", log, api.DeploymentValues.AI.BridgeConfig) + return total +} + +func createAnthropicMessagesReq(t *testing.T, baseURL string, input string) *http.Request { + t.Helper() + + req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/v1/messages", strings.NewReader(input)) require.NoError(t, err) - api.AIBridgeDaemons = []*aibridged.Server{bridgeSrv} - - // Wait for bridge server to start listening - require.Eventually(t, func() bool { - return len(api.AIBridgeDaemons) > 0 && api.AIBridgeDaemons[0].BridgeAddr() != "" - }, testutil.WaitMedium, testutil.IntervalFast) - - body := []byte(` -{ - "model": "gpt-4.1", - "stream": true, - "messages": [ - { - "role": "user", - "content": "What is the weather like in Cape Town today?" - } - ] -}`) - - resp, err := client.Request(t.Context(), http.MethodPost, "/api/v2/aibridge/v1/chat/completions", body) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "text/event-stream") + + return req +} + +func getSessionToken(t *testing.T) string { + t.Helper() + + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + resp, err := client.LoginWithPassword(t.Context(), codersdk.LoginWithPasswordRequest{ + Email: coderdtest.FirstUserParams.Email, + Password: coderdtest.FirstUserParams.Password, + }) + require.NoError(t, err) + return resp.SessionToken +} - require.Equal(t, http.StatusOK, resp.StatusCode) +type fakeSSEServer struct { + *httptest.Server } -type fakeBridgeDaemonClient struct{} +func newFakeSSEServer(ctx context.Context, fixture []byte) *fakeSSEServer { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set SSE headers + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + // Stream the fixture content with random intervals + scanner := bufio.NewScanner(bytes.NewReader(fixture)) + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + return + } -func (f *fakeBridgeDaemonClient) DRPCConn() drpc.Conn { + for scanner.Scan() { + line := scanner.Text() + + // Write the line + fmt.Fprintf(w, "%s\n", line) + flusher.Flush() + + // // Add random delay between 10-50ms to mimic real streaming + // delay := time.Duration(rand.Intn(40)+10) * time.Millisecond + // select { + // case <-ctx.Done(): + // return + // case <-time.After(delay): + // // Continue + // } + } + + if err := scanner.Err(); err != nil { + http.Error(w, fmt.Sprintf("Error reading fixture: %v", err), http.StatusInternalServerError) + return + } + })) + + srv.Config.BaseContext = func(_ net.Listener) context.Context { + return ctx + } + + return &fakeSSEServer{ + Server: srv, + } +} + +type fakeBridgeDaemonClient struct { + mu sync.Mutex + + tokenUsages []*proto.TrackTokenUsageRequest + userPrompts []*proto.TrackUserPromptRequest + toolUsages []*proto.TrackToolUsageRequest +} + +func (*fakeBridgeDaemonClient) DRPCConn() drpc.Conn { conn, _ := drpcsdk.MemTransportPipe() return conn } -func (f *fakeBridgeDaemonClient) AuditPrompt(ctx context.Context, in *proto.AuditPromptRequest) (*proto.AuditPromptResponse, error) { - return nil, xerrors.New("not implemented") -} + func (f *fakeBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { - return nil, xerrors.New("not implemented") + f.mu.Lock() + defer f.mu.Unlock() + f.tokenUsages = append(f.tokenUsages, in) + + return &proto.TrackTokenUsageResponse{}, nil } -func (f *fakeBridgeDaemonClient) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPromptsRequest) (*proto.TrackUserPromptsResponse, error) { - return nil, xerrors.New("not implemented") + +func (f *fakeBridgeDaemonClient) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptRequest) (*proto.TrackUserPromptResponse, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.userPrompts = append(f.userPrompts, in) + + return &proto.TrackUserPromptResponse{}, nil +} + +func (f *fakeBridgeDaemonClient) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageRequest) (*proto.TrackToolUsageResponse, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.toolUsages = append(f.toolUsages, in) + + return &proto.TrackToolUsageResponse{}, nil } diff --git a/aibridged/fixtures/anthropic/single_builtin_tool.txtar b/aibridged/fixtures/anthropic/single_builtin_tool.txtar new file mode 100644 index 0000000000000..fd87d3cac2b7e --- /dev/null +++ b/aibridged/fixtures/anthropic/single_builtin_tool.txtar @@ -0,0 +1,45 @@ +Claude Code has builtin tools to (e.g.) explore the filesystem. + +-- request -- +{ + "model": "claude-sonnet-4-20250514", + "max_tokens": 1024, + "messages": [ + { + "role": "user", + "content": "read the foo file" + } + ], + "stream": true +} + +-- response -- +event: message_start +data: {"type":"message_start","message":{"id":"msg_015SQewixvT9s4cABCVvUE6g","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22,"cache_read_input_tokens":13993,"output_tokens":5,"service_tier":"standard"}} } + +event: content_block_start +data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_01RX68weRSquLx6HUTj65iBo","name":"Read","input":{}} } + +event: ping +data: {"type": "ping"} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/foo"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\"}"} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":61} } + +event: message_stop +data: {"type":"message_stop" } + + +--- diff --git a/aibridged/mcp.go b/aibridged/mcp.go index fac21a5f859b3..899472a2ac9cc 100644 --- a/aibridged/mcp.go +++ b/aibridged/mcp.go @@ -4,13 +4,14 @@ import ( "context" "fmt" - "cdr.dev/slog" "github.com/anthropics/anthropic-sdk-go" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/client/transport" "github.com/mark3labs/mcp-go/mcp" "golang.org/x/exp/maps" "golang.org/x/xerrors" + + "cdr.dev/slog" ) type MCPToolBridge struct { @@ -23,18 +24,18 @@ type MCPToolBridge struct { const MCPProxyDelimiter = "_" func NewMCPToolBridge(name, serverURL string, headers map[string]string, logger slog.Logger) (*MCPToolBridge, error) { - //ts := transport.NewMemoryTokenStore() - //if err := ts.SaveToken(&transport.Token{ + // ts := transport.NewMemoryTokenStore() + // if err := ts.SaveToken(&transport.Token{ // AccessToken: token, - //}); err != nil { + // }); err != nil { // return nil, xerrors.Errorf("save token: %w", err) //} mcpClient, err := client.NewStreamableHttpClient(serverURL, transport.WithHTTPHeaders(headers)) - //transport.WithHTTPOAuth(transport.OAuthConfig{ + // transport.WithHTTPOAuth(transport.OAuthConfig{ // TokenStore: ts, - //})) + // })) if err != nil { return nil, xerrors.Errorf("create streamable http client: %w", err) } @@ -65,7 +66,9 @@ func (b *MCPToolBridge) ListTools() []anthropic.BetaToolUnionParam { } func (b *MCPToolBridge) HasTool(name string) bool { - if b.foundTools == nil { return false } + if b.foundTools == nil { + return false + } _, ok := b.foundTools[name] return ok diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index 5dbc51dcf773b..dcdff3425d84f 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -20,99 +20,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type AuditPromptRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Prompt string `protobuf:"bytes,1,opt,name=prompt,proto3" json:"prompt,omitempty"` - Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` -} - -func (x *AuditPromptRequest) Reset() { - *x = AuditPromptRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *AuditPromptRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AuditPromptRequest) ProtoMessage() {} - -func (x *AuditPromptRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AuditPromptRequest.ProtoReflect.Descriptor instead. -func (*AuditPromptRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} -} - -func (x *AuditPromptRequest) GetPrompt() string { - if x != nil { - return x.Prompt - } - return "" -} - -func (x *AuditPromptRequest) GetProvider() string { - if x != nil { - return x.Provider - } - return "" -} - -type AuditPromptResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *AuditPromptResponse) Reset() { - *x = AuditPromptResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *AuditPromptResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AuditPromptResponse) ProtoMessage() {} - -func (x *AuditPromptResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AuditPromptResponse.ProtoReflect.Descriptor instead. -func (*AuditPromptResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} -} - type TrackTokenUsageRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -128,7 +35,7 @@ type TrackTokenUsageRequest struct { func (x *TrackTokenUsageRequest) Reset() { *x = TrackTokenUsageRequest{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -141,7 +48,7 @@ func (x *TrackTokenUsageRequest) String() string { func (*TrackTokenUsageRequest) ProtoMessage() {} func (x *TrackTokenUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -154,7 +61,7 @@ func (x *TrackTokenUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackTokenUsageRequest.ProtoReflect.Descriptor instead. func (*TrackTokenUsageRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} } func (x *TrackTokenUsageRequest) GetMsgId() string { @@ -201,7 +108,7 @@ type TrackTokenUsageResponse struct { func (x *TrackTokenUsageResponse) Reset() { *x = TrackTokenUsageResponse{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -214,7 +121,7 @@ func (x *TrackTokenUsageResponse) String() string { func (*TrackTokenUsageResponse) ProtoMessage() {} func (x *TrackTokenUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -227,10 +134,10 @@ func (x *TrackTokenUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackTokenUsageResponse.ProtoReflect.Descriptor instead. func (*TrackTokenUsageResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{3} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} } -type TrackUserPromptsRequest struct { +type TrackUserPromptRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -239,23 +146,23 @@ type TrackUserPromptsRequest struct { Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` } -func (x *TrackUserPromptsRequest) Reset() { - *x = TrackUserPromptsRequest{} +func (x *TrackUserPromptRequest) Reset() { + *x = TrackUserPromptRequest{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *TrackUserPromptsRequest) String() string { +func (x *TrackUserPromptRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TrackUserPromptsRequest) ProtoMessage() {} +func (*TrackUserPromptRequest) ProtoMessage() {} -func (x *TrackUserPromptsRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] +func (x *TrackUserPromptRequest) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -266,48 +173,48 @@ func (x *TrackUserPromptsRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TrackUserPromptsRequest.ProtoReflect.Descriptor instead. -func (*TrackUserPromptsRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{4} +// Deprecated: Use TrackUserPromptRequest.ProtoReflect.Descriptor instead. +func (*TrackUserPromptRequest) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} } -func (x *TrackUserPromptsRequest) GetPrompt() string { +func (x *TrackUserPromptRequest) GetPrompt() string { if x != nil { return x.Prompt } return "" } -func (x *TrackUserPromptsRequest) GetModel() string { +func (x *TrackUserPromptRequest) GetModel() string { if x != nil { return x.Model } return "" } -type TrackUserPromptsResponse struct { +type TrackUserPromptResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } -func (x *TrackUserPromptsResponse) Reset() { - *x = TrackUserPromptsResponse{} +func (x *TrackUserPromptResponse) Reset() { + *x = TrackUserPromptResponse{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *TrackUserPromptsResponse) String() string { +func (x *TrackUserPromptResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TrackUserPromptsResponse) ProtoMessage() {} +func (*TrackUserPromptResponse) ProtoMessage() {} -func (x *TrackUserPromptsResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] +func (x *TrackUserPromptResponse) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -318,9 +225,9 @@ func (x *TrackUserPromptsResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TrackUserPromptsResponse.ProtoReflect.Descriptor instead. -func (*TrackUserPromptsResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} +// Deprecated: Use TrackUserPromptResponse.ProtoReflect.Descriptor instead. +func (*TrackUserPromptResponse) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{3} } type TrackToolUsageRequest struct { @@ -337,7 +244,7 @@ type TrackToolUsageRequest struct { func (x *TrackToolUsageRequest) Reset() { *x = TrackToolUsageRequest{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -350,7 +257,7 @@ func (x *TrackToolUsageRequest) String() string { func (*TrackToolUsageRequest) ProtoMessage() {} func (x *TrackToolUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -363,7 +270,7 @@ func (x *TrackToolUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackToolUsageRequest.ProtoReflect.Descriptor instead. func (*TrackToolUsageRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{6} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{4} } func (x *TrackToolUsageRequest) GetTool() string { @@ -403,7 +310,7 @@ type TrackToolUsageResponse struct { func (x *TrackToolUsageResponse) Reset() { *x = TrackToolUsageResponse{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -416,7 +323,7 @@ func (x *TrackToolUsageResponse) String() string { func (*TrackToolUsageResponse) ProtoMessage() {} func (x *TrackToolUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -429,7 +336,7 @@ func (x *TrackToolUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackToolUsageResponse.ProtoReflect.Descriptor instead. func (*TrackToolUsageResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{7} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} } var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor @@ -437,80 +344,69 @@ var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x0a, 0x1f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x09, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x12, - 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x13, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8b, 0x02, - 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, - 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, - 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x42, 0x0a, - 0x05, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, - 0x4f, 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x6f, 0x74, 0x68, 0x65, - 0x72, 0x1a, 0x38, 0x0a, 0x0a, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, - 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x47, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, - 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, - 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x22, - 0x1a, 0x0a, 0x18, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, - 0x70, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xda, 0x01, 0x0a, 0x15, - 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, - 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, - 0x41, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, - 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, - 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x6e, 0x70, - 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x1a, 0x38, - 0x0a, 0x0a, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x32, 0xec, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, - 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x1d, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, - 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, - 0x41, 0x75, 0x64, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, - 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, - 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, - 0x10, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x73, 0x12, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, - 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, - 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, - 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, - 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, - 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, - 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, - 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x12, 0x09, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x22, 0x8b, 0x02, 0x0a, + 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, + 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x42, 0x0a, 0x05, + 0x6f, 0x74, 0x68, 0x65, 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x69, + 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, + 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x6f, 0x74, 0x68, 0x65, 0x72, + 0x1a, 0x38, 0x0a, 0x0a, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, + 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x46, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, + 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x19, 0x0a, + 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xda, 0x01, 0x0a, 0x15, 0x54, 0x72, 0x61, + 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x41, 0x0a, 0x05, + 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x69, + 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, + 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6e, + 0x70, 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, + 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x1a, 0x38, 0x0a, 0x0a, 0x49, + 0x6e, 0x70, 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, + 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, + 0x9b, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, + 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, + 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, + 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, + 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, + 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x69, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, + 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, + 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -525,32 +421,28 @@ func file_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { return file_aibridged_proto_aibridged_proto_rawDescData } -var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ - (*AuditPromptRequest)(nil), // 0: aibridged.AuditPromptRequest - (*AuditPromptResponse)(nil), // 1: aibridged.AuditPromptResponse - (*TrackTokenUsageRequest)(nil), // 2: aibridged.TrackTokenUsageRequest - (*TrackTokenUsageResponse)(nil), // 3: aibridged.TrackTokenUsageResponse - (*TrackUserPromptsRequest)(nil), // 4: aibridged.TrackUserPromptsRequest - (*TrackUserPromptsResponse)(nil), // 5: aibridged.TrackUserPromptsResponse - (*TrackToolUsageRequest)(nil), // 6: aibridged.TrackToolUsageRequest - (*TrackToolUsageResponse)(nil), // 7: aibridged.TrackToolUsageResponse - nil, // 8: aibridged.TrackTokenUsageRequest.OtherEntry - nil, // 9: aibridged.TrackToolUsageRequest.InputEntry + (*TrackTokenUsageRequest)(nil), // 0: aibridged.TrackTokenUsageRequest + (*TrackTokenUsageResponse)(nil), // 1: aibridged.TrackTokenUsageResponse + (*TrackUserPromptRequest)(nil), // 2: aibridged.TrackUserPromptRequest + (*TrackUserPromptResponse)(nil), // 3: aibridged.TrackUserPromptResponse + (*TrackToolUsageRequest)(nil), // 4: aibridged.TrackToolUsageRequest + (*TrackToolUsageResponse)(nil), // 5: aibridged.TrackToolUsageResponse + nil, // 6: aibridged.TrackTokenUsageRequest.OtherEntry + nil, // 7: aibridged.TrackToolUsageRequest.InputEntry } var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ - 8, // 0: aibridged.TrackTokenUsageRequest.other:type_name -> aibridged.TrackTokenUsageRequest.OtherEntry - 9, // 1: aibridged.TrackToolUsageRequest.input:type_name -> aibridged.TrackToolUsageRequest.InputEntry - 0, // 2: aibridged.AIBridgeDaemon.AuditPrompt:input_type -> aibridged.AuditPromptRequest - 2, // 3: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest - 4, // 4: aibridged.AIBridgeDaemon.TrackUserPrompts:input_type -> aibridged.TrackUserPromptsRequest - 6, // 5: aibridged.AIBridgeDaemon.TrackToolUsage:input_type -> aibridged.TrackToolUsageRequest - 1, // 6: aibridged.AIBridgeDaemon.AuditPrompt:output_type -> aibridged.AuditPromptResponse - 3, // 7: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse - 5, // 8: aibridged.AIBridgeDaemon.TrackUserPrompts:output_type -> aibridged.TrackUserPromptsResponse - 7, // 9: aibridged.AIBridgeDaemon.TrackToolUsage:output_type -> aibridged.TrackToolUsageResponse - 6, // [6:10] is the sub-list for method output_type - 2, // [2:6] is the sub-list for method input_type + 6, // 0: aibridged.TrackTokenUsageRequest.other:type_name -> aibridged.TrackTokenUsageRequest.OtherEntry + 7, // 1: aibridged.TrackToolUsageRequest.input:type_name -> aibridged.TrackToolUsageRequest.InputEntry + 0, // 2: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest + 2, // 3: aibridged.AIBridgeDaemon.TrackUserPrompt:input_type -> aibridged.TrackUserPromptRequest + 4, // 4: aibridged.AIBridgeDaemon.TrackToolUsage:input_type -> aibridged.TrackToolUsageRequest + 1, // 5: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse + 3, // 6: aibridged.AIBridgeDaemon.TrackUserPrompt:output_type -> aibridged.TrackUserPromptResponse + 5, // 7: aibridged.AIBridgeDaemon.TrackToolUsage:output_type -> aibridged.TrackToolUsageResponse + 5, // [5:8] is the sub-list for method output_type + 2, // [2:5] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name @@ -563,7 +455,7 @@ func file_aibridged_proto_aibridged_proto_init() { } if !protoimpl.UnsafeEnabled { file_aibridged_proto_aibridged_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuditPromptRequest); i { + switch v := v.(*TrackTokenUsageRequest); i { case 0: return &v.state case 1: @@ -575,7 +467,7 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuditPromptResponse); i { + switch v := v.(*TrackTokenUsageResponse); i { case 0: return &v.state case 1: @@ -587,7 +479,7 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackTokenUsageRequest); i { + switch v := v.(*TrackUserPromptRequest); i { case 0: return &v.state case 1: @@ -599,7 +491,7 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackTokenUsageResponse); i { + switch v := v.(*TrackUserPromptResponse); i { case 0: return &v.state case 1: @@ -611,30 +503,6 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackUserPromptsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_aibridged_proto_aibridged_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackUserPromptsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_aibridged_proto_aibridged_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TrackToolUsageRequest); i { case 0: return &v.state @@ -646,7 +514,7 @@ func file_aibridged_proto_aibridged_proto_init() { return nil } } - file_aibridged_proto_aibridged_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_aibridged_proto_aibridged_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TrackToolUsageResponse); i { case 0: return &v.state @@ -665,7 +533,7 @@ func file_aibridged_proto_aibridged_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_aibridged_proto_aibridged_proto_rawDesc, NumEnums: 0, - NumMessages: 10, + NumMessages: 8, NumExtensions: 0, NumServices: 1, }, diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index 572b5473765c7..64fac90b2b256 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -3,13 +3,6 @@ option go_package = "github.com/coder/coder/v2/aibridged/proto"; package aibridged; -message AuditPromptRequest { - string prompt = 1; - string provider = 2; -} - -message AuditPromptResponse {} - message TrackTokenUsageRequest { string msg_id = 1; int64 input_tokens = 2; @@ -19,11 +12,11 @@ message TrackTokenUsageRequest { } message TrackTokenUsageResponse {} -message TrackUserPromptsRequest { +message TrackUserPromptRequest { string prompt = 1; string model = 2; } -message TrackUserPromptsResponse {} +message TrackUserPromptResponse {} message TrackToolUsageRequest { string tool = 1; @@ -35,11 +28,7 @@ message TrackToolUsageResponse {} // AIBridgeDaemon describes the service exposed by coderd, which allows the AI bridge to send data to coderd. service AIBridgeDaemon { - - // TODO: expand to full auditing; this is just a toy implementation for the moment. - // AuditPrompt allows aibridged to report back to coderd that a prompt was issued by a user to an AI provider. - rpc AuditPrompt(AuditPromptRequest) returns (AuditPromptResponse); rpc TrackTokenUsage(TrackTokenUsageRequest) returns (TrackTokenUsageResponse); - rpc TrackUserPrompts(TrackUserPromptsRequest) returns (TrackUserPromptsResponse); + rpc TrackUserPrompt(TrackUserPromptRequest) returns (TrackUserPromptResponse); rpc TrackToolUsage(TrackToolUsageRequest) returns (TrackToolUsageResponse); } diff --git a/aibridged/proto/aibridged_drpc.pb.go b/aibridged/proto/aibridged_drpc.pb.go index 33efc227104f9..dd38e9a007aef 100644 --- a/aibridged/proto/aibridged_drpc.pb.go +++ b/aibridged/proto/aibridged_drpc.pb.go @@ -38,9 +38,8 @@ func (drpcEncoding_File_aibridged_proto_aibridged_proto) JSONUnmarshal(buf []byt type DRPCAIBridgeDaemonClient interface { DRPCConn() drpc.Conn - AuditPrompt(ctx context.Context, in *AuditPromptRequest) (*AuditPromptResponse, error) TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) - TrackUserPrompts(ctx context.Context, in *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) + TrackUserPrompt(ctx context.Context, in *TrackUserPromptRequest) (*TrackUserPromptResponse, error) TrackToolUsage(ctx context.Context, in *TrackToolUsageRequest) (*TrackToolUsageResponse, error) } @@ -54,15 +53,6 @@ func NewDRPCAIBridgeDaemonClient(cc drpc.Conn) DRPCAIBridgeDaemonClient { func (c *drpcAIBridgeDaemonClient) DRPCConn() drpc.Conn { return c.cc } -func (c *drpcAIBridgeDaemonClient) AuditPrompt(ctx context.Context, in *AuditPromptRequest) (*AuditPromptResponse, error) { - out := new(AuditPromptResponse) - err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/AuditPrompt", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) - if err != nil { - return nil, err - } - return out, nil -} - func (c *drpcAIBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) { out := new(TrackTokenUsageResponse) err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackTokenUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) @@ -72,9 +62,9 @@ func (c *drpcAIBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *Trac return out, nil } -func (c *drpcAIBridgeDaemonClient) TrackUserPrompts(ctx context.Context, in *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) { - out := new(TrackUserPromptsResponse) - err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackUserPrompts", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) +func (c *drpcAIBridgeDaemonClient) TrackUserPrompt(ctx context.Context, in *TrackUserPromptRequest) (*TrackUserPromptResponse, error) { + out := new(TrackUserPromptResponse) + err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackUserPrompt", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -91,23 +81,18 @@ func (c *drpcAIBridgeDaemonClient) TrackToolUsage(ctx context.Context, in *Track } type DRPCAIBridgeDaemonServer interface { - AuditPrompt(context.Context, *AuditPromptRequest) (*AuditPromptResponse, error) TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) - TrackUserPrompts(context.Context, *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) + TrackUserPrompt(context.Context, *TrackUserPromptRequest) (*TrackUserPromptResponse, error) TrackToolUsage(context.Context, *TrackToolUsageRequest) (*TrackToolUsageResponse, error) } type DRPCAIBridgeDaemonUnimplementedServer struct{} -func (s *DRPCAIBridgeDaemonUnimplementedServer) AuditPrompt(context.Context, *AuditPromptRequest) (*AuditPromptResponse, error) { - return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) -} - func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) { return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } -func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackUserPrompts(context.Context, *TrackUserPromptsRequest) (*TrackUserPromptsResponse, error) { +func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackUserPrompt(context.Context, *TrackUserPromptRequest) (*TrackUserPromptResponse, error) { return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } @@ -117,20 +102,11 @@ func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackToolUsage(context.Context, type DRPCAIBridgeDaemonDescription struct{} -func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 4 } +func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 3 } func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { case 0: - return "/aibridged.AIBridgeDaemon/AuditPrompt", drpcEncoding_File_aibridged_proto_aibridged_proto{}, - func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { - return srv.(DRPCAIBridgeDaemonServer). - AuditPrompt( - ctx, - in1.(*AuditPromptRequest), - ) - }, DRPCAIBridgeDaemonServer.AuditPrompt, true - case 1: return "/aibridged.AIBridgeDaemon/TrackTokenUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAIBridgeDaemonServer). @@ -139,16 +115,16 @@ func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc. in1.(*TrackTokenUsageRequest), ) }, DRPCAIBridgeDaemonServer.TrackTokenUsage, true - case 2: - return "/aibridged.AIBridgeDaemon/TrackUserPrompts", drpcEncoding_File_aibridged_proto_aibridged_proto{}, + case 1: + return "/aibridged.AIBridgeDaemon/TrackUserPrompt", drpcEncoding_File_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAIBridgeDaemonServer). - TrackUserPrompts( + TrackUserPrompt( ctx, - in1.(*TrackUserPromptsRequest), + in1.(*TrackUserPromptRequest), ) - }, DRPCAIBridgeDaemonServer.TrackUserPrompts, true - case 3: + }, DRPCAIBridgeDaemonServer.TrackUserPrompt, true + case 2: return "/aibridged.AIBridgeDaemon/TrackToolUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAIBridgeDaemonServer). @@ -166,22 +142,6 @@ func DRPCRegisterAIBridgeDaemon(mux drpc.Mux, impl DRPCAIBridgeDaemonServer) err return mux.Register(impl, DRPCAIBridgeDaemonDescription{}) } -type DRPCAIBridgeDaemon_AuditPromptStream interface { - drpc.Stream - SendAndClose(*AuditPromptResponse) error -} - -type drpcAIBridgeDaemon_AuditPromptStream struct { - drpc.Stream -} - -func (x *drpcAIBridgeDaemon_AuditPromptStream) SendAndClose(m *AuditPromptResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { - return err - } - return x.CloseSend() -} - type DRPCAIBridgeDaemon_TrackTokenUsageStream interface { drpc.Stream SendAndClose(*TrackTokenUsageResponse) error @@ -198,16 +158,16 @@ func (x *drpcAIBridgeDaemon_TrackTokenUsageStream) SendAndClose(m *TrackTokenUsa return x.CloseSend() } -type DRPCAIBridgeDaemon_TrackUserPromptsStream interface { +type DRPCAIBridgeDaemon_TrackUserPromptStream interface { drpc.Stream - SendAndClose(*TrackUserPromptsResponse) error + SendAndClose(*TrackUserPromptResponse) error } -type drpcAIBridgeDaemon_TrackUserPromptsStream struct { +type drpcAIBridgeDaemon_TrackUserPromptStream struct { drpc.Stream } -func (x *drpcAIBridgeDaemon_TrackUserPromptsStream) SendAndClose(m *TrackUserPromptsResponse) error { +func (x *drpcAIBridgeDaemon_TrackUserPromptStream) SendAndClose(m *TrackUserPromptResponse) error { if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { return err } diff --git a/aibridged/proxy.go b/aibridged/proxy.go deleted file mode 100644 index a95b31b500d89..0000000000000 --- a/aibridged/proxy.go +++ /dev/null @@ -1,391 +0,0 @@ -package aibridged - -import ( - "bufio" - "bytes" - "context" - "io" - "net/http" - "net/http/httputil" - "net/url" - "strings" - "sync" - "time" - - "golang.org/x/xerrors" -) - -// ResponseInterceptFunc is called for each chunk of data received from the upstream server. -// For streaming responses, it's called for each SSE event or data chunk. -// For non-streaming responses, it's called once with the complete response body. -// The function receives a copy of the data - modifications don't affect the response. -type ResponseInterceptFunc func(sess *OpenAISession, data []byte, isStreaming bool) ([][]byte, bool, error) - -// RequestInterceptFunc is called for each request before it's sent to the upstream server. -// The function receives a copy of the request body - modifications don't affect the request. -type RequestInterceptFunc func(req *http.Request, body []byte) error - -// RequestModifyFunc is called to modify the request body before sending to upstream. -// Unlike RequestInterceptFunc, this function can modify the request body. -// Returns the modified body or an error. -type RequestModifyFunc func(req *http.Request, body []byte) ([]byte, error) - -// SSEProxy provides a fast, memory-efficient proxy using httputil.ReverseProxy. -// It supports efficient copying of responses for interception without buffering. -type SSEProxy struct { - *httputil.ReverseProxy - responseInterceptFunc ResponseInterceptFunc - requestInterceptFunc RequestInterceptFunc - requestModifyFunc RequestModifyFunc - bufferPool sync.Pool - config *ProxyConfig -} - -// NewSSEProxy creates a new SSE proxy that proxies to the given target URL. -func NewSSEProxy(target *url.URL, responseInterceptFunc ResponseInterceptFunc) *SSEProxy { - return NewSSEProxyWithRequestIntercept(target, responseInterceptFunc, nil) -} - -// NewSSEProxyWithRequestIntercept creates a new SSE proxy with both request and response interception. -func NewSSEProxyWithRequestIntercept(target *url.URL, responseInterceptFunc ResponseInterceptFunc, requestInterceptFunc RequestInterceptFunc) *SSEProxy { - proxy := &SSEProxy{ - responseInterceptFunc: responseInterceptFunc, - requestInterceptFunc: requestInterceptFunc, - bufferPool: sync.Pool{ - New: func() interface{} { - // Use 32KB buffers for optimal performance - return make([]byte, 32*1024) - }, - }, - } - - proxy.ReverseProxy = httputil.NewSingleHostReverseProxy(target) - - // Configure for optimal streaming performance - proxy.FlushInterval = -1 // Immediate flushing - - // Custom transport for streaming optimization - proxy.Transport = &http.Transport{ - DisableCompression: true, // Disable compression for streaming - MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, - ResponseHeaderTimeout: 0, // No timeout for streaming responses - } - - // Custom director for request interception - originalDirector := proxy.Director - proxy.Director = func(req *http.Request) { - originalDirector(req) - - // Intercept request if function is provided - if proxy.requestInterceptFunc != nil { - proxy.interceptRequest(req) - } - } - - // Custom response modifier for interception - originalModifyResponse := proxy.ModifyResponse - proxy.ModifyResponse = func(resp *http.Response) error { - // Call original modifier first if it exists - if originalModifyResponse != nil { - if err := originalModifyResponse(resp); err != nil { - return err - } - } - - // Wrap the response body for interception - resp.Body = proxy.wrapResponseBody(resp.Body, resp.Header) - return nil - } - - return proxy -} - -// interceptRequest intercepts and processes the request body -func (p *SSEProxy) interceptRequest(req *http.Request) { - if req.Body == nil { - // Call intercept function with empty body - if p.requestInterceptFunc != nil { - if err := p.requestInterceptFunc(req, nil); err != nil { - // Log error but continue - _ = err - } - } - return - } - - // Read the request body - body, err := io.ReadAll(req.Body) - if err != nil { - // Log error but continue - _ = err - return - } - - // Close the original body - req.Body.Close() - - // Create a copy for interception - bodyCopy := make([]byte, len(body)) - copy(bodyCopy, body) - - // Call intercept function if provided - if p.requestInterceptFunc != nil { - if err := p.requestInterceptFunc(req, bodyCopy); err != nil { - // Log error but continue - _ = err - } - } - - // Apply request modifications if provided - finalBody := body - if p.requestModifyFunc != nil { - modifiedBody, err := p.requestModifyFunc(req, bodyCopy) - if err != nil { - // Log error but continue with original body - _ = err - } else { - finalBody = modifiedBody - } - } - - // Restore the request body for the upstream request - req.Body = io.NopCloser(bytes.NewReader(finalBody)) - req.ContentLength = int64(len(finalBody)) -} - -// wrapResponseBody wraps the response body to enable interception -func (p *SSEProxy) wrapResponseBody(body io.ReadCloser, headers http.Header) io.ReadCloser { - if p.responseInterceptFunc == nil { - return body - } - - // Determine if this is a streaming response - contentType := headers.Get("Content-Type") - isStreaming := strings.Contains(contentType, "text/event-stream") || - strings.Contains(contentType, "text/plain") || - headers.Get("Transfer-Encoding") == "chunked" - - if isStreaming { - return &streamingInterceptReader{ - ReadCloser: body, - interceptFunc: p.responseInterceptFunc, - bufferPool: &p.bufferPool, - sess: p.config.OpenAISession, - } - } - - return &nonStreamingInterceptReader{ - ReadCloser: body, - interceptFunc: p.responseInterceptFunc, - } -} - -// streamingInterceptReader intercepts streaming responses chunk by chunk -type streamingInterceptReader struct { - io.ReadCloser - interceptFunc ResponseInterceptFunc - bufferPool *sync.Pool - reader *bufio.Reader - readerOnce sync.Once - - sess *OpenAISession - trailerEvents [][]byte -} - -func (sir *streamingInterceptReader) Read(p []byte) (n int, err error) { - // Initialize buffered reader once - sir.readerOnce.Do(func() { - sir.reader = bufio.NewReader(sir.ReadCloser) - }) - - for { - n, err = sir.reader.Read(p) - if n > 0 { - // Call intercept function with a copy of the chunk - chunk := make([]byte, n) - copy(chunk, p[:n]) - - // Once [DONE] is found, we need to inject the trailer events and rewind the reader. - if bytes.Contains(chunk, []byte(`[DONE]`)) { - // TODO: inject trailers then [DONE] events. - // continue - } - - extraEvents, send, interceptErr := sir.interceptFunc(sir.sess, chunk, true) - if interceptErr != nil { - // TODO: Log error but continue reading - _ = interceptErr - } - - if !send { - continue - } - sir.trailerEvents = extraEvents - } - - break - } - return n, err -} - -// nonStreamingInterceptReader intercepts complete non-streaming responses -type nonStreamingInterceptReader struct { - io.ReadCloser - interceptFunc ResponseInterceptFunc - buffer bytes.Buffer - intercepted bool - mu sync.Mutex -} - -func (nsir *nonStreamingInterceptReader) Read(p []byte) (n int, err error) { - n, err = nsir.ReadCloser.Read(p) - - if n > 0 { - nsir.mu.Lock() - nsir.buffer.Write(p[:n]) - nsir.mu.Unlock() - } - - // If we've reached EOF and haven't intercepted yet, do it now - if err == io.EOF && !nsir.intercepted { - nsir.mu.Lock() - if !nsir.intercepted { - nsir.intercepted = true - data := nsir.buffer.Bytes() - nsir.mu.Unlock() - - if _, _, interceptErr := nsir.interceptFunc(nil, data, false); interceptErr != nil { - // Log error but continue - _ = interceptErr - } - } else { - nsir.mu.Unlock() - } - } - - return n, err -} - -// ProxyConfig holds configuration for the SSE proxy -type ProxyConfig struct { - Target *url.URL - ResponseInterceptFunc ResponseInterceptFunc - RequestInterceptFunc RequestInterceptFunc - RequestModifyFunc RequestModifyFunc - ModifyRequest func(*http.Request) - ModifyResponse func(*http.Response) error - ErrorHandler func(http.ResponseWriter, *http.Request, error) - Transport http.RoundTripper - FlushInterval time.Duration // -1 for immediate flushing - - OpenAISession *OpenAISession -} - -// NewSSEProxyWithConfig creates a new SSE proxy with custom configuration -func NewSSEProxyWithConfig(config ProxyConfig) (*SSEProxy, error) { - if config.Target == nil { - return nil, xerrors.Errorf("target URL is required") - } - - proxy := &SSEProxy{ - responseInterceptFunc: config.ResponseInterceptFunc, - requestInterceptFunc: config.RequestInterceptFunc, - requestModifyFunc: config.RequestModifyFunc, - bufferPool: sync.Pool{ - New: func() interface{} { - return make([]byte, 32*1024) - }, - }, - - config: &config, - } - - proxy.ReverseProxy = httputil.NewSingleHostReverseProxy(config.Target) - - // Configure flush interval - if config.FlushInterval != 0 { - proxy.FlushInterval = config.FlushInterval - } else { - proxy.FlushInterval = -1 // Default to immediate flushing - } - - // Configure transport - if config.Transport != nil { - proxy.Transport = config.Transport - } else { - proxy.Transport = &http.Transport{ - DisableCompression: true, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, - ResponseHeaderTimeout: 0, - } - } - - // Configure request modifier and interception - originalDirector := proxy.Director - proxy.Director = func(req *http.Request) { - originalDirector(req) - - req.Host = config.Target.Host - req.URL.Scheme = config.Target.Scheme - req.URL.Host = config.Target.Host - - // Apply custom request modifications first - if config.ModifyRequest != nil { - config.ModifyRequest(req) - } - - // Then apply request interception and modification - if proxy.requestInterceptFunc != nil || proxy.requestModifyFunc != nil { - proxy.interceptRequest(req) - } - } - - // Configure response modifier - proxy.ModifyResponse = func(resp *http.Response) error { - // Call custom modifier first if it exists - if config.ModifyResponse != nil { - if err := config.ModifyResponse(resp); err != nil { - return err - } - } - - // Wrap the response body for interception - resp.Body = proxy.wrapResponseBody(resp.Body, resp.Header) - return nil - } - - // Configure error handler - if config.ErrorHandler != nil { - proxy.ErrorHandler = config.ErrorHandler - } - - return proxy, nil -} - -// ServeHTTPWithContext serves HTTP requests with context for cancellation -func (p *SSEProxy) ServeHTTPWithContext(ctx context.Context, w http.ResponseWriter, r *http.Request) { - // Create a new request with the given context - r = r.WithContext(ctx) - - // Set up cancellation monitoring - done := make(chan struct{}) - go func() { - select { - case <-ctx.Done(): - // Context canceled, try to close the connection - if hijacker, ok := w.(http.Hijacker); ok { - if conn, _, err := hijacker.Hijack(); err == nil { - conn.Close() - } - } - case <-done: - // Request completed normally - } - }() - - // Serve the request - p.ServeHTTP(w, r) - close(done) -} diff --git a/aibridged/sse_parser.go b/aibridged/sse_parser.go new file mode 100644 index 0000000000000..63417cd1d9e60 --- /dev/null +++ b/aibridged/sse_parser.go @@ -0,0 +1,124 @@ +package aibridged + +import ( + "bufio" + "io" + "strconv" + "strings" + "sync" +) + +const ( + SSEEventTypeMessage = "message" + SSEEventTypeError = "error" + SSEEventTypePing = "ping" +) + +type SSEEvent struct { + Type string + Data string + ID string + Retry int +} + +type SSEParser struct { + events map[string][]SSEEvent + mu sync.RWMutex +} + +func NewSSEParser() *SSEParser { + return &SSEParser{ + events: make(map[string][]SSEEvent), + } +} + +func (p *SSEParser) Parse(reader io.Reader) error { + scanner := bufio.NewScanner(reader) + + var currentEvent SSEEvent + var dataLines []string + + for scanner.Scan() { + line := scanner.Text() + + // Empty line indicates end of event + if line == "" { + if len(dataLines) > 0 { + currentEvent.Data = strings.Join(dataLines, "\n") + } + + // Default to message type if no event type specified + if currentEvent.Type == "" { + currentEvent.Type = SSEEventTypeMessage + } + + // Store the event + p.mu.Lock() + p.events[currentEvent.Type] = append(p.events[currentEvent.Type], currentEvent) + p.mu.Unlock() + + // Reset for next event + currentEvent = SSEEvent{} + dataLines = nil + continue + } + + // Skip comments + if strings.HasPrefix(line, ":") { + continue + } + + // Parse field:value format + if colonIndex := strings.Index(line, ":"); colonIndex != -1 { + field := line[:colonIndex] + value := line[colonIndex+1:] + + // Remove leading space from value if present + if len(value) > 0 && value[0] == ' ' { + value = value[1:] + } + + switch field { + case "event": + currentEvent.Type = value + case "data": + dataLines = append(dataLines, value) + case "id": + currentEvent.ID = value + case "retry": + if retryMs, err := strconv.Atoi(value); err == nil { + currentEvent.Retry = retryMs + } + } + } + } + + return scanner.Err() +} + +func (p *SSEParser) EventsByType(eventType string) []SSEEvent { + p.mu.RLock() + defer p.mu.RUnlock() + + events := p.events[eventType] + result := make([]SSEEvent, len(events)) + copy(result, events) + return result +} + +func (p *SSEParser) MessageEvents() []SSEEvent { + return p.EventsByType(SSEEventTypeMessage) +} + +func (p *SSEParser) AllEvents() map[string][]SSEEvent { + p.mu.RLock() + defer p.mu.RUnlock() + + result := make(map[string][]SSEEvent) + for eventType, events := range p.events { + eventsCopy := make([]SSEEvent, len(events)) + copy(eventsCopy, events) + result[eventType] = eventsCopy + } + return result +} diff --git a/aibridged/streaming.go b/aibridged/streaming.go index f469bea6a0fb2..a466d2d0a3f48 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -69,14 +69,10 @@ func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, model string, return } - // TODO: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/implement-tool-use#example-of-successful-tool-result see "Important formatting requirements" - - - // TODO: use logger, make configurable. - //_, _ = fmt.Fprintf(os.Stderr, "[%s] %s", sessionID, payload) + // _, _ = fmt.Fprintf(os.Stderr, "[%s] %s", sessionID, payload) _, _ = os.Stderr.Write([]byte(fmt.Sprintf("[orig] %s\n[zero] %s\n[out] %s", ev.orig, ev.zero, ev.payload))) _, err := w.Write(ev.payload) diff --git a/aibridged/util/zero_marshaler.go b/aibridged/util/zero_marshaler.go index fd71b4b95b474..32b0690383654 100644 --- a/aibridged/util/zero_marshaler.go +++ b/aibridged/util/zero_marshaler.go @@ -2,15 +2,15 @@ package util import ( "encoding/json" - "errors" "fmt" "reflect" "strings" "github.com/jmespath/go-jmespath" + "golang.org/x/xerrors" ) -var ErrEmptyPath = errors.New("empty path provided") +var ErrEmptyPath = xerrors.New("empty path provided") // MarshalNoZero serializes v to JSON, omitting any struct field whose entire // value graph is zero **unless** the field carries a `no_omit` tag or matches @@ -80,7 +80,7 @@ func createTestObject(path string) (map[string]any, error) { if nextMap, ok := arr[idx].(map[string]any); ok { current = nextMap } else { - return nil, fmt.Errorf("invalid path structure at %s", part) + return nil, xerrors.Errorf("invalid path structure at %s", part) } } } @@ -92,7 +92,7 @@ func createTestObject(path string) (map[string]any, error) { if nextMap, ok := current[part].(map[string]any); ok { current = nextMap } else { - return nil, fmt.Errorf("invalid path structure at %s", part) + return nil, xerrors.Errorf("invalid path structure at %s", part) } } } diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index 0f571bbc49622..759550a12f8e8 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -18,10 +18,6 @@ type Server struct { store database.Store } -func (s *Server) AuditPrompt(_ context.Context, req *proto.AuditPromptRequest) (*proto.AuditPromptResponse, error) { - return &proto.AuditPromptResponse{}, nil -} - func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { raw, err := json.Marshal(in) if err != nil { @@ -36,7 +32,7 @@ func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageR return &proto.TrackTokenUsageResponse{}, nil } -func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPromptsRequest) (*proto.TrackUserPromptsResponse, error) { +func (s *Server) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptRequest) (*proto.TrackUserPromptResponse, error) { raw, err := json.Marshal(in) if err != nil { return nil, xerrors.Errorf("marshal event: %w", err) @@ -47,7 +43,7 @@ func (s *Server) TrackUserPrompts(ctx context.Context, in *proto.TrackUserPrompt return nil, xerrors.Errorf("store event: %w", err) } - return &proto.TrackUserPromptsResponse{}, nil + return &proto.TrackUserPromptResponse{}, nil } func (s *Server) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageRequest) (*proto.TrackToolUsageResponse, error) { diff --git a/coderd/database/migrations/000349_aibridge.up.sql b/coderd/database/migrations/000350_aibridge.up.sql similarity index 100% rename from coderd/database/migrations/000349_aibridge.up.sql rename to coderd/database/migrations/000350_aibridge.up.sql diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 4e0c84bcee89f..2cade74cc86a2 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3136,18 +3136,29 @@ Write out the current server config as YAML to stdout.`, Value: &c.AI.BridgeConfig.OpenAIBaseURL, Default: "https://api.openai.com", Group: &deploymentGroupAIBridge, - YAML: "daemons", + YAML: "openai_base_url", Hidden: true, }, { Name: "AI Bridge Anthropic Base URL", Description: "TODO.", Flag: "ai-bridge-anthropic-base-url", - Env: "CODER_AI_BRIDGE_Anthropic_BASE_URL", - Value: &c.AI.BridgeConfig.AnthropicBaseURL, + Env: "CODER_AI_BRIDGE_ANTHROPIC_BASE_URL", + Value: &c.AI.BridgeConfig.Anthropic.BaseURL, Default: "https://api.anthropic.com", Group: &deploymentGroupAIBridge, - YAML: "daemons", + YAML: "base_url", + Hidden: true, + }, + { + Name: "AI Bridge Anthropic KEY", + Description: "TODO.", + Flag: "ai-bridge-anthropic-key", + Env: "CODER_AI_BRIDGE_ANTHROPIC_KEY", + Value: &c.AI.BridgeConfig.Anthropic.Key, + Default: "https://api.anthropic.com", + Group: &deploymentGroupAIBridge, + YAML: "key", Hidden: true, }, } @@ -3156,9 +3167,14 @@ Write out the current server config as YAML to stdout.`, } type AIBridgeConfig struct { - Daemons serpent.Int64 `json:"daemons" typescript:",notnull"` - OpenAIBaseURL serpent.String `json:"openai_base_url" typescript:",notnull"` - AnthropicBaseURL serpent.String `json:"anthropic_base_url" typescript:",notnull"` + Daemons serpent.Int64 `json:"daemons" typescript:",notnull"` + OpenAIBaseURL serpent.String `json:"openai_base_url" typescript:",notnull"` + Anthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"` +} + +type AIBridgeAnthropicConfig struct { + BaseURL serpent.String `json:"base_url" typescript:",notnull"` + Key serpent.String `json:"key" typescript:",notnull"` } type AIConfig struct { diff --git a/go.mod b/go.mod index cdac6323b104d..9e6f141836b2e 100644 --- a/go.mod +++ b/go.mod @@ -486,7 +486,6 @@ require ( github.com/coder/aisdk-go v0.0.9 github.com/coder/preview v1.0.3-0.20250701142654-c3d6e86b9393 github.com/fsnotify/fsnotify v1.9.0 - github.com/invopop/jsonschema v0.13.0 github.com/mark3labs/mcp-go v0.33.0 github.com/openai/openai-go v1.8.1 ) @@ -508,9 +507,7 @@ require ( github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aquasecurity/trivy v0.58.2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect @@ -533,7 +530,6 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/tmaxmax/go-sse v0.11.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect diff --git a/go.sum b/go.sum index 4a8af6291f244..f89f42497d9eb 100644 --- a/go.sum +++ b/go.sum @@ -790,8 +790,6 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= @@ -830,8 +828,6 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM= github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g= @@ -1417,8 +1413,6 @@ github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwso github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o= github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -1855,8 +1849,6 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b6543f3326037..e808f144d3eb4 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -6,11 +6,17 @@ export interface ACLAvailable { readonly groups: readonly Group[]; } +// From codersdk/deployment.go +export interface AIBridgeAnthropicConfig { + readonly base_url: string; + readonly key: string; +} + // From codersdk/deployment.go export interface AIBridgeConfig { readonly daemons: number; readonly openai_base_url: string; - readonly anthropic_base_url: string; + readonly anthropic: AIBridgeAnthropicConfig; } // From codersdk/deployment.go From 1dd71941919158c0234fd843f9adca251749dfc6 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 17 Jul 2025 15:14:08 +0200 Subject: [PATCH 32/61] non-streaming handling & test Signed-off-by: Danny Kopping --- aibridged/bridge.go | 83 +++++- aibridged/bridge_test.go | 240 +++++++++++------- .../anthropic/single_builtin_tool.txtar | 88 ++++++- 3 files changed, 314 insertions(+), 97 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 1003e5124dad0..d9c055f88a8c2 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -601,7 +601,88 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") client := anthropic.NewClient(opts...) if !in.UseStreaming() { - http.Error(w, "streaming API supported only", http.StatusBadRequest) + var resp *anthropic.BetaMessage + for { + resp, err = client.Beta.Messages.New(ctx, messages, opts...) + if err != nil { + if isConnectionError(err) { + b.logger.Warn(ctx, "upstream connection closed", slog.Error(err)) + return + } + + b.logger.Error(ctx, "anthropic stream error", slog.Error(err)) + if antErr := getAnthropicErrorResponse(err); antErr != nil { + fmt.Println("oops") + } + } + + if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + MsgId: resp.ID, + Model: string(resp.Model), + InputTokens: resp.Usage.InputTokens, + OutputTokens: resp.Usage.OutputTokens, + Other: map[string]int64{ + "web_search_requests": resp.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": resp.Usage.CacheCreationInputTokens, + "cache_read_input": resp.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": resp.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": resp.Usage.CacheCreation.Ephemeral5mInputTokens, + }, + }); err != nil { + b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) + } + + messages.Messages = append(messages.Messages, resp.ToParam()) + + if resp.StopReason == anthropic.BetaStopReasonEndTurn { + break + } + + if resp.StopReason == anthropic.BetaStopReasonToolUse { + var ( + toolUse anthropic.BetaToolUseBlock + input any + ) + for _, c := range resp.Content { + toolUse = c.AsToolUse() + if toolUse.ID == "" { + continue + } + + input = toolUse.Input + } + + if input != nil { + var ( + serialized map[string]string + buf bytes.Buffer + ) + _ = json.NewEncoder(&buf).Encode(input) + _ = json.NewDecoder(&buf).Decode(&serialized) + + _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + Model: string(resp.Model), + Input: serialized, + Tool: toolUse.Name, + }) + if err != nil { + b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + } + } + + break + } + } + + out, err := json.Marshal(resp) + if err != nil { + http.Error(w, "error marshaling response", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(out) return } diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index 240ec942cfcf9..7a69a256398ef 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -9,7 +9,6 @@ import ( "net" "net/http" "net/http/httptest" - "strings" "sync" "testing" @@ -33,7 +32,13 @@ var ( antSingleBuiltinTool []byte ) -func TestAnthropicMessagesStreaming(t *testing.T) { +const ( + FixtureRequest = "request" + FixtureStreamingResponse = "streaming" + FixtureNonStreamingResponse = "non-streaming" +) + +func TestAnthropicMessages(t *testing.T) { t.Parallel() sessionToken := getSessionToken(t) @@ -41,59 +46,93 @@ func TestAnthropicMessagesStreaming(t *testing.T) { t.Run("single builtin tool", func(t *testing.T) { t.Parallel() - arc := txtar.Parse(antSingleBuiltinTool) - t.Logf("%s: %s", t.Name(), arc.Comment) - require.Len(t, arc.Files, 2) - reqBody, respBody := arc.Files[0], arc.Files[1] - - ctx := testutil.Context(t, testutil.WaitLong) - srv := newFakeSSEServer(ctx, respBody.Data) - t.Cleanup(srv.Close) - - coderdClient := &fakeBridgeDaemonClient{} - - logger := testutil.Logger(t) - b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - Anthropic: codersdk.AIBridgeAnthropicConfig{ - BaseURL: serpent.String(srv.URL), - Key: serpent.String(sessionToken), + cases := []struct { + streaming bool + }{ + // { + // streaming: true, + // }, + { + streaming: false, }, - }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return coderdClient, true - }) - require.NoError(t, err) - - go func() { - assert.NoError(t, b.Serve()) - }() - // Wait for bridge to come up. - require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) - - // Make API call to aibridge for Anthropic /v1/messages - req := createAnthropicMessagesReq(t, "http://"+b.Addr(), string(reqBody.Data)) - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - sp := aibridged.NewSSEParser() - require.NoError(t, sp.Parse(resp.Body)) - - assert.Contains(t, sp.AllEvents(), "message_start") - assert.Contains(t, sp.AllEvents(), "message_stop") - - require.Len(t, coderdClient.tokenUsages, 2) // One from message_start, one from message_delta. - assert.NotZero(t, calculateTotalInputTokens(coderdClient.tokenUsages)) - assert.NotZero(t, calculateTotalOutputTokens(coderdClient.tokenUsages)) - - require.Len(t, coderdClient.toolUsages, 1) - assert.Equal(t, "Read", coderdClient.toolUsages[0].Tool) - require.Contains(t, coderdClient.toolUsages[0].Input, "file_path") - assert.Equal(t, "/tmp/blah/foo", coderdClient.toolUsages[0].Input["file_path"]) - - require.Len(t, coderdClient.userPrompts, 1) - assert.Equal(t, "read the foo file", coderdClient.userPrompts[0].Prompt) + } + + for _, tc := range cases { + arc := txtar.Parse(antSingleBuiltinTool) + t.Logf("%s: %s", t.Name(), arc.Comment) + + files := filesMap(arc) + require.Len(t, files, 3) + require.Contains(t, files, FixtureRequest) + require.Contains(t, files, FixtureStreamingResponse) + require.Contains(t, files, FixtureNonStreamingResponse) + + // Replace macro to indicate whether request is streaming or not. + reqBody := files[FixtureRequest] + require.Contains(t, string(reqBody), "%STREAMING%", "missing %STREAMING% macro in request") + reqBody = bytes.Replace(reqBody, []byte("%STREAMING%"), fmt.Appendf(nil, "%v", tc.streaming), 1) + + ctx := testutil.Context(t, testutil.WaitLong) + srv := newMockServer(ctx, files) + t.Cleanup(srv.Close) + + coderdClient := &fakeBridgeDaemonClient{} + + logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + Anthropic: codersdk.AIBridgeAnthropicConfig{ + BaseURL: serpent.String(srv.URL), + Key: serpent.String(sessionToken), + }, + }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return coderdClient, true + }) + require.NoError(t, err) + + go func() { + assert.NoError(t, b.Serve()) + }() + // Wait for bridge to come up. + require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) + + // Make API call to aibridge for Anthropic /v1/messages + req := createAnthropicMessagesReq(t, "http://"+b.Addr(), reqBody) + if tc.streaming { + req.Header.Set("Accept", "text/event-stream") + } else { + req.Header.Set("Accept", "application/json") + } + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() + + // Response-specific checks. + if tc.streaming { + sp := aibridged.NewSSEParser() + require.NoError(t, sp.Parse(resp.Body)) + + // Ensure the message starts and completes, at a minimum. + assert.Contains(t, sp.AllEvents(), "message_start") + assert.Contains(t, sp.AllEvents(), "message_stop") + require.Len(t, coderdClient.tokenUsages, 2) // One from message_start, one from message_delta. + } else { + require.Len(t, coderdClient.tokenUsages, 1) + } + + assert.NotZero(t, calculateTotalInputTokens(coderdClient.tokenUsages)) + assert.NotZero(t, calculateTotalOutputTokens(coderdClient.tokenUsages)) + + require.Len(t, coderdClient.toolUsages, 1) + assert.Equal(t, "Read", coderdClient.toolUsages[0].Tool) + require.Contains(t, coderdClient.toolUsages[0].Input, "file_path") + assert.Equal(t, "/tmp/blah/foo", coderdClient.toolUsages[0].Input["file_path"]) + + require.Len(t, coderdClient.userPrompts, 1) + assert.Equal(t, "read the foo file", coderdClient.userPrompts[0].Prompt) + } }) } @@ -113,13 +152,26 @@ func calculateTotalInputTokens(in []*proto.TrackTokenUsageRequest) int64 { return total } -func createAnthropicMessagesReq(t *testing.T, baseURL string, input string) *http.Request { +type archiveFileMap map[string][]byte + +func filesMap(archive *txtar.Archive) archiveFileMap { + if len(archive.Files) == 0 { + return nil + } + + out := make(archiveFileMap, len(archive.Files)) + for _, f := range archive.Files { + out[f.Name] = f.Data + } + return out +} + +func createAnthropicMessagesReq(t *testing.T, baseURL string, input []byte) *http.Request { t.Helper() - req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/v1/messages", strings.NewReader(input)) + req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/v1/messages", bytes.NewReader(input)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "text/event-stream") return req } @@ -138,46 +190,46 @@ func getSessionToken(t *testing.T) string { return resp.SessionToken } -type fakeSSEServer struct { +type mockServer struct { *httptest.Server } -func newFakeSSEServer(ctx context.Context, fixture []byte) *fakeSSEServer { +func newMockServer(ctx context.Context, files archiveFileMap) *mockServer { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Set SSE headers - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") - - // Stream the fixture content with random intervals - scanner := bufio.NewScanner(bytes.NewReader(fixture)) - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "Streaming unsupported", http.StatusInternalServerError) - return - } - - for scanner.Scan() { - line := scanner.Text() - - // Write the line - fmt.Fprintf(w, "%s\n", line) - flusher.Flush() - - // // Add random delay between 10-50ms to mimic real streaming - // delay := time.Duration(rand.Intn(40)+10) * time.Millisecond - // select { - // case <-ctx.Done(): - // return - // case <-time.After(delay): - // // Continue - // } - } - - if err := scanner.Err(); err != nil { - http.Error(w, fmt.Sprintf("Error reading fixture: %v", err), http.StatusInternalServerError) - return + contentType := r.Header.Get("Accept") + switch contentType { + // SSE + case "text/event-stream": + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + scanner := bufio.NewScanner(bytes.NewReader(files[FixtureStreamingResponse])) + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + return + } + + for scanner.Scan() { + line := scanner.Text() + + fmt.Fprintf(w, "%s\n", line) + flusher.Flush() + } + + if err := scanner.Err(); err != nil { + http.Error(w, fmt.Sprintf("Error reading fixture: %v", err), http.StatusInternalServerError) + return + } + case "application/json": + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(files[FixtureNonStreamingResponse]) + + default: + panic(fmt.Sprintf("unsupported content type: %q", contentType)) } })) @@ -185,7 +237,7 @@ func newFakeSSEServer(ctx context.Context, fixture []byte) *fakeSSEServer { return ctx } - return &fakeSSEServer{ + return &mockServer{ Server: srv, } } diff --git a/aibridged/fixtures/anthropic/single_builtin_tool.txtar b/aibridged/fixtures/anthropic/single_builtin_tool.txtar index fd87d3cac2b7e..55f7f043d9ac0 100644 --- a/aibridged/fixtures/anthropic/single_builtin_tool.txtar +++ b/aibridged/fixtures/anthropic/single_builtin_tool.txtar @@ -10,10 +10,10 @@ Claude Code has builtin tools to (e.g.) explore the filesystem. "content": "read the foo file" } ], - "stream": true + "stream": %STREAMING% } --- response -- +-- streaming -- event: message_start data: {"type":"message_start","message":{"id":"msg_015SQewixvT9s4cABCVvUE6g","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22,"cache_read_input_tokens":13993,"output_tokens":5,"service_tier":"standard"}} } @@ -42,4 +42,88 @@ event: message_stop data: {"type":"message_stop" } +-- non-streaming -- +{ + "id": "msg_01JHKqEmh7wYuPXqUWUvusfL", + "container": { + "id": "", + "expires_at": "0001-01-01T00:00:00Z" + }, + "content": [ + { + "citations": null, + "text": "I can see there's a file named `foo` in the `/tmp/blah` directory. Let me read it.", + "type": "text", + "id": "", + "input": null, + "name": "", + "content": { + "OfBetaWebSearchResultBlockArray": null, + "OfString": "", + "OfBetaMCPToolResultBlockContent": null, + "error_code": "", + "type": "", + "content": null, + "return_code": 0, + "stderr": "", + "stdout": "" + }, + "tool_use_id": "", + "server_name": "", + "is_error": false, + "file_id": "", + "signature": "", + "thinking": "", + "data": "" + }, + { + "citations": null, + "text": "", + "type": "tool_use", + "id": "toolu_01AusGgY5aKFhzWrFBv9JfHq", + "input": { + "file_path": "/tmp/blah/foo" + }, + "name": "Read", + "content": { + "OfBetaWebSearchResultBlockArray": null, + "OfString": "", + "OfBetaMCPToolResultBlockContent": null, + "error_code": "", + "type": "", + "content": null, + "return_code": 0, + "stderr": "", + "stdout": "" + }, + "tool_use_id": "", + "server_name": "", + "is_error": false, + "file_id": "", + "signature": "", + "thinking": "", + "data": "" + } + ], + "model": "claude-sonnet-4-20250514", + "role": "assistant", + "stop_reason": "tool_use", + "stop_sequence": "", + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 23490, + "input_tokens": 5, + "output_tokens": 84, + "server_tool_use": { + "web_search_requests": 0 + }, + "service_tier": "standard" + } +} + --- From e4654b73f30b31b23bb59e9bb7d6dfc3024c83ae Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 22 Jul 2025 17:13:44 +0200 Subject: [PATCH 33/61] removed zero marshaler, removed openai tool impl, simplifications all round Signed-off-by: Danny Kopping --- aibridged/bridge.go | 205 ++------------ aibridged/streaming.go | 47 +--- aibridged/util/zero_marshaler.go | 309 --------------------- aibridged/util/zero_marshaler_test.go | 385 -------------------------- 4 files changed, 39 insertions(+), 907 deletions(-) delete mode 100644 aibridged/util/zero_marshaler.go delete mode 100644 aibridged/util/zero_marshaler_test.go diff --git a/aibridged/bridge.go b/aibridged/bridge.go index d9c055f88a8c2..7ae0c9d1e2350 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -58,6 +58,18 @@ func (e *BridgeError) Error() string { return e.Message } +// Bridge is responsible for proxying requests to upstream AI providers. +// +// Characteristics: +// 1. Client-side cancel +// 2. No timeout (SSE) +// 3a. client<->coderd conn established +// 3b. coderd<-> provider conn established +// 4a. requests from client<->coderd must be parsed, augmented, and relayed +// 4b. responses from provider->coderd must be parsed, optionally reflected back to client +// 5. tool calls may be injected and intercepted, transparently to the client +// 6. multiple calls can be made to provider while holding client<->coderd conn open +// 7. client<->coderd conn must ONLY be closed on client-side disconn or coderd<->provider non-recoverable error. type Bridge struct { cfg codersdk.AIBridgeConfig @@ -80,7 +92,12 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli srv := &http.Server{ Addr: addr, Handler: mux, - // TODO: other settings. + + // TODO: configurable. + ReadTimeout: 30 * time.Second, + WriteTimeout: 0, // No write timeout for streaming responses. + IdleTimeout: 120 * time.Second, + ReadHeaderTimeout: 10 * time.Second, } bridge.cfg = cfg @@ -148,6 +165,10 @@ func (b *Bridge) openAITarget() *url.URL { return target } +// proxyOpenAIRequest intercepts, filters, augments, and delivers requests & responses from client to upstream and back. +// +// References: +// - https://platform.openai.com/docs/api-reference/chat-streaming func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { sessionID := uuid.New() b.logger.Info(r.Context(), "OpenAI request started", slog.F("sessionID", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) @@ -161,16 +182,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) }() - // Required characteristics: - // 1. Client-side cancel - // 2. No timeout (SSE) - // 3a. client->coderd conn established - // 3b. coderd->AI provider conn established - // 4. responses from AI provider->coderd must be parsed, optionally reflected back to client - // 5. tool calls must be injected and intercepted, transparently to the client - // 6. multiple calls can be made to AI provider while holding client->coderd conn open - // 7. client->coderd conn must ONLY be closed on client-side disconn or coderd->AI provider non-recoverable error. - // Allow us to interrupt watch via cancel. ctx, cancel := context.WithCancel(r.Context()) defer cancel() @@ -216,23 +227,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), }, in.Messages...) - in.Tools = []openai.ChatCompletionToolParam{ - { - Function: openai.FunctionDefinitionParam{ - Name: "get_weather", - Description: openai.String("Get weather at the given location"), - Parameters: openai.FunctionParameters{ - "type": "object", - "properties": map[string]interface{}{ - "location": map[string]string{ - "type": "string", - }, - }, - "required": []string{"location"}, - }, - }, - }, - } + // TODO: implement MCP tool injection. client := openai.NewClient() @@ -240,7 +235,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { streamCtx, streamCancel := context.WithCancelCause(ctx) defer streamCancel(xerrors.New("deferred")) - es := newEventStream(openAIEventStream) + events := newEventStream(openAIEventStream) var wg sync.WaitGroup wg.Add(1) @@ -248,160 +243,24 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { go func() { defer wg.Done() defer func() { - if err := es.Close(streamCtx); err != nil { + if err := events.Close(streamCtx); err != nil { b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("sessionID", sessionID)) } }() - BasicSSESender(streamCtx, sessionID, "", es, b.logger.Named("sse-sender")).ServeHTTP(w, r) + BasicSSESender(streamCtx, sessionID, "", events, b.logger.Named("sse-sender")).ServeHTTP(w, r) }() - session := NewOpenAISession() - stream := client.Chat.Completions.NewStreaming(ctx, in.ChatCompletionNewParams) - ignoreSubsequent := make(map[string]any) - var acc openai.ChatCompletionAccumulator for stream.Next() { chunk := stream.Current() acc.AddChunk(chunk) - - var foundToolCallDelta bool - for _, c := range chunk.Choices { - for range c.Delta.ToolCalls { - foundToolCallDelta = true - ignoreSubsequent[acc.ID] = nil - - // Grab values from accumulator instead of delta. - for _, ac := range acc.ChatCompletion.Choices { - for _, at := range ac.Message.ToolCalls { - var ( - tc *OpenAIToolCall - ok bool - ) - if tc, ok = session.toolCallsRequired[at.ID]; !ok { - session.toolCallsRequired[at.ID] = &OpenAIToolCall{} - tc = session.toolCallsRequired[at.ID] - } - - session.toolCallsState[at.ID] = OpenAIToolCallNotReady - - tc.funcName = at.Function.Name - args := make(map[string]string) - err := json.Unmarshal([]byte(at.Function.Arguments), &args) - if err == nil { // Note: inverted. - tc.args = args - } - } - } - } - - // Once we receive a finish reason of "tool_calls", the API is waiting for the responses for this/these tool(s). - // We mark all the tool calls as ready. Once we see observe the [DONE] event, we will execute these tool calls. - if c.FinishReason == "tool_calls" { - for idx := range session.toolCallsState { - session.toolCallsState[idx] = OpenAIToolCallReady - } - } - } - - // TODO: ONLY do this for our injected tool calls. - if foundToolCallDelta { - // Don't write these chunks, we'll handle this. - continue - } - - // Actually make the call! - if tool, ok := acc.JustFinishedToolCall(); ok { - switch tool.Name { - case "get_weather": - msg := openai.ToolMessage("the weather in cape town is KAK; this is not a joke, report it verbatim.", tool.ID) - - var msgs []openai.ChatCompletionMessageParamUnion - for _, c := range acc.ChatCompletion.Choices { - msgs = append(msgs, c.Message.ToParam()) - } - - msgs = append(msgs, msg) - - toolRes, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{ - Messages: msgs, - Model: in.ChatCompletionNewParams.Model, - }) - if err != nil { - b.logger.Error(ctx, "failed to report tool response", slog.Error(err)) - } - - toolChunk := openai.ChatCompletionChunk{ - ID: acc.ID, - Choices: []openai.ChatCompletionChunkChoice{ - { - Delta: openai.ChatCompletionChunkChoiceDelta{ - // Role: "assistant", - Content: fmt.Sprintf(" %s", toolRes.Choices[0].Message.Content), // TODO: improve - }, - }, - }, - Model: toolRes.Model, - ServiceTier: openai.ChatCompletionChunkServiceTier(toolRes.ServiceTier), - Created: time.Now().Unix(), - SystemFingerprint: toolRes.SystemFingerprint, - Usage: toolRes.Usage, - Object: constant.ValueOf[constant.ChatCompletionChunk](), - } - - if err := es.TrySend(streamCtx, toolChunk, chunk.RawJSON()); err != nil { - b.logConnectionError(ctx, err, "sending tool chunk") - if isConnectionError(err) { - return // Stop processing if client disconnected - } - } - - finishChunk := openai.ChatCompletionChunk{ - ID: acc.ID, - Choices: []openai.ChatCompletionChunkChoice{ - { - Delta: openai.ChatCompletionChunkChoiceDelta{ - // Role: "assistant", - Content: "", - }, - FinishReason: string(openai.CompletionChoiceFinishReasonStop), - }, - }, - Model: toolRes.Model, - ServiceTier: openai.ChatCompletionChunkServiceTier(toolRes.ServiceTier), - Created: time.Now().Unix(), - SystemFingerprint: toolRes.SystemFingerprint, - Usage: toolRes.Usage, - Object: constant.ValueOf[constant.ChatCompletionChunk](), - } - - if err := es.TrySend(streamCtx, finishChunk, chunk.RawJSON(), "choices[].delta.content"); err != nil { - b.logConnectionError(ctx, err, "sending finish chunk") - if isConnectionError(err) { - return // Stop processing if client disconnected - } - } - } - continue - } - - // TODO: clean this up. Once we receive a tool invocation we need to hijack the conversation, since the client - // won't be handling the tool call if auto-injected. That means that any subsequent events which wrap - // up the stream need to be ignored because we send those after the tool call is executed and the result - // is appended as if it came from the assistant. - if _, ok := ignoreSubsequent[acc.ID]; !ok { - if err := es.TrySend(streamCtx, chunk, chunk.RawJSON()); err != nil { - b.logConnectionError(ctx, err, "sending reflected chunk") - if isConnectionError(err) { - return // Stop processing if client disconnected - } - } - } + events.TrySend(ctx, chunk) } - if err := es.Close(streamCtx); err != nil { + if err := events.Close(streamCtx); err != nil { b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) } @@ -475,9 +334,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) }() - // out, _ := httputil.DumpRequest(r, true) - // fmt.Printf("\n\nREQUEST: %s\n\n", out) - // Allow us to interrupt watch via cancel. ctx, cancel := context.WithCancel(r.Context()) defer cancel() @@ -903,7 +759,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { var hasValidResult bool for _, content := range res.Content { - switch cb := content.(type) { case mcp.TextContent: // messages.Messages = append(messages.Messages, @@ -1028,7 +883,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } - if err := es.TrySend(streamCtx, event, event.RawJSON()); err != nil { + if err := es.TrySend(streamCtx, event); err != nil { b.logConnectionError(ctx, err, "sending event") if isConnectionError(err) { return // Stop processing if client disconnected @@ -1044,7 +899,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { b.logger.Error(ctx, "anthropic stream error", slog.Error(streamErr)) if antErr := getAnthropicErrorResponse(streamErr); antErr != nil { - err = es.TrySend(streamCtx, antErr, "") + err = es.TrySend(streamCtx, antErr) if err != nil { b.logger.Error(ctx, "failed to send error", slog.Error(err)) } diff --git a/aibridged/streaming.go b/aibridged/streaming.go index a466d2d0a3f48..c7f6b496c6328 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "os" "strings" "sync" "syscall" @@ -17,8 +16,6 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" - - "github.com/coder/coder/v2/aibridged/util" ) // isConnectionError checks if an error is related to client disconnection @@ -69,13 +66,7 @@ func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, model string, return } - // TODO: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/implement-tool-use#example-of-successful-tool-result see "Important formatting requirements" - - // TODO: use logger, make configurable. - // _, _ = fmt.Fprintf(os.Stderr, "[%s] %s", sessionID, payload) - _, _ = os.Stderr.Write([]byte(fmt.Sprintf("[orig] %s\n[zero] %s\n[out] %s", ev.orig, ev.zero, ev.payload))) - - _, err := w.Write(ev.payload) + _, err := w.Write(ev) if err != nil { if isConnectionError(err) { logger.Debug(ctx, "client disconnected during SSE write", slog.Error(err)) @@ -105,17 +96,13 @@ func flush(w http.ResponseWriter) { } type EventStreamer interface { - TrySend(ctx context.Context, data any, input string, exclusions ...string) error + TrySend(ctx context.Context, data any) error Events() <-chan event Close(ctx context.Context) error Closed() <-chan any } -type event struct { - payload []byte - zero []byte // Marshaling with zero-value elements omitted. - orig string -} +type event []byte type eventStream struct { eventsCh chan event @@ -148,7 +135,7 @@ func (s *eventStream) Closed() <-chan any { return s.closedCh } -func (s *eventStream) TrySend(ctx context.Context, data any, input string, exclusions ...string) error { +func (s *eventStream) TrySend(ctx context.Context, data any) error { // Save an unnecessary marshaling if possible. select { case <-ctx.Done(): @@ -158,31 +145,15 @@ func (s *eventStream) TrySend(ctx context.Context, data any, input string, exclu default: } - var ( - payload []byte - err error - ) - switch s.kind { - case openAIEventStream: - // https://github.com/openai/openai-go#request-fields - // I noticed that Cursor would bork if it received streaming response payloads which had zero values. - // I'm not sure if this is a Cursor-specific issue or more widespread, but I've vibed a marshaler which will filter - // out all the zero value objects in the response, with optional exclusions. - payload, err = util.MarshalNoZero(data, exclusions...) - default: - payload, err = json.Marshal(data) - } - - zero, _ := util.MarshalNoZero(data, exclusions...) - + payload, err := json.Marshal(data) if err != nil { return xerrors.Errorf("marshal payload: %w", err) } - return s.send(ctx, payload, zero, input) + return s.send(ctx, payload) } -func (s *eventStream) send(ctx context.Context, payload, zero []byte, input string) error { +func (s *eventStream) send(ctx context.Context, payload []byte) error { switch s.kind { case openAIEventStream: var buf bytes.Buffer @@ -217,7 +188,7 @@ func (s *eventStream) send(ctx context.Context, payload, zero []byte, input stri return ctx.Err() case <-s.closedCh: return xerrors.New("closed") - case s.eventsCh <- event{payload: payload, orig: input, zero: zero}: + case s.eventsCh <- payload: return nil } } @@ -227,7 +198,7 @@ func (s *eventStream) Close(ctx context.Context) error { s.closedOnce.Do(func() { switch s.kind { case openAIEventStream: - err := s.send(ctx, []byte("[DONE]"), nil, "") + err := s.send(ctx, []byte("[DONE]")) if err != nil { out = xerrors.Errorf("close stream: %w", err) } diff --git a/aibridged/util/zero_marshaler.go b/aibridged/util/zero_marshaler.go deleted file mode 100644 index 32b0690383654..0000000000000 --- a/aibridged/util/zero_marshaler.go +++ /dev/null @@ -1,309 +0,0 @@ -package util - -import ( - "encoding/json" - "fmt" - "reflect" - "strings" - - "github.com/jmespath/go-jmespath" - "golang.org/x/xerrors" -) - -var ErrEmptyPath = xerrors.New("empty path provided") - -// MarshalNoZero serializes v to JSON, omitting any struct field whose entire -// value graph is zero **unless** the field carries a `no_omit` tag or matches -// one of the provided JMESPath exclusion expressions. -func MarshalNoZero(v any, exclusions ...string) ([]byte, error) { - cleaned, zero := prune(reflect.ValueOf(v), false, exclusions, "") - if zero { - return []byte("null"), nil - } - return json.Marshal(cleaned) -} - -// matchesExclusion checks if the current path matches any of the JMESPath exclusion patterns. -func matchesExclusion(path string, exclusions []string) bool { - if len(exclusions) == 0 || path == "" { - return false - } - - // Create a temporary object with the path structure to test against - // For example, if path is "user.profile.name", we create {"user":{"profile":{"name":true}}} - testObj, err := createTestObject(path) - if err != nil { - return false - } - - for _, exclusion := range exclusions { - result, err := jmespath.Search(exclusion, testObj) - if err != nil { - continue - } - if result != nil { - return true - } - } - return false -} - -// createTestObject creates a nested object structure from a dot-notation path -// for testing against JMESPath expressions. -func createTestObject(path string) (map[string]any, error) { - if path == "" { - return nil, ErrEmptyPath - } - - parts := strings.Split(path, ".") - result := make(map[string]any) - current := result - - for i, part := range parts { - // Handle array notation like "items[0]" - if strings.Contains(part, "[") && strings.Contains(part, "]") { - // Extract array name and index - arrayName := part[:strings.Index(part, "[")] - indexStr := part[strings.Index(part, "[")+1 : strings.Index(part, "]")] - - // Create array structure - var arr []any - if idx := parseArrayIndex(indexStr); idx >= 0 { - // Create array with enough elements - for j := 0; j <= idx; j++ { - arr = append(arr, make(map[string]any)) - } - current[arrayName] = arr - if i == len(parts)-1 { - arr[idx] = true // Mark as matched for final element - } else { - if nextMap, ok := arr[idx].(map[string]any); ok { - current = nextMap - } else { - return nil, xerrors.Errorf("invalid path structure at %s", part) - } - } - } - } else { - if i == len(parts)-1 { - current[part] = true // Mark final element as matched - } else { - current[part] = make(map[string]any) - if nextMap, ok := current[part].(map[string]any); ok { - current = nextMap - } else { - return nil, xerrors.Errorf("invalid path structure at %s", part) - } - } - } - } - - return result, nil -} - -// parseArrayIndex extracts numeric index from array notation, returns -1 if invalid. -func parseArrayIndex(indexStr string) int { - // Simple numeric parsing - extend if needed for more complex patterns - if indexStr == "*" { - return 0 // Treat wildcard as index 0 for testing - } - - var idx int - if _, err := fmt.Sscanf(indexStr, "%d", &idx); err == nil { - return idx - } - return -1 -} - -// prune walks the value tree and returns (cleanedValue, isZeroTree). -// `forceKeep` is true when an ancestor field has the `no_omit` tag or matches an exclusion. -// `exclusions` contains JMESPath expressions to exclude from zero-omission. -// `currentPath` is the current JSON path being processed. -func prune(v reflect.Value, forceKeep bool, exclusions []string, currentPath string) (any, bool) { - if !v.IsValid() { - return nil, true - } - - // Allow custom IsZero() overrides. - if v.CanInterface() { - if z, ok := v.Interface().(interface{ IsZero() bool }); ok && z.IsZero() && !forceKeep { - return nil, true - } - } - - switch v.Kind() { - case reflect.Pointer, reflect.Interface: - if v.IsNil() { - if forceKeep { - return nil, false - } - return nil, true - } - return prune(v.Elem(), forceKeep, exclusions, currentPath) - - case reflect.Struct: - out := make(map[string]any) - allZero := true - vt := v.Type() - - for i := 0; i < v.NumField(); i++ { - sf := vt.Field(i) - if sf.PkgPath != "" { // unexported - continue - } - - jName, jOpts := parseJSONTag(sf.Tag.Get("json")) - if jName == "-" { - continue - } - if jName == "" { - jName = sf.Name - } - - // Build path for this field - fieldPath := currentPath - if fieldPath == "" { - fieldPath = jName - } else { - fieldPath = fieldPath + "." + jName - } - - childForce := forceKeep || hasNoOmit(sf.Tag, jOpts) || matchesExclusion(fieldPath, exclusions) - val, zero := prune(v.Field(i), childForce, exclusions, fieldPath) - if zero && !childForce { - continue - } - allZero = false - out[jName] = val - } - - if allZero && !forceKeep { - return nil, true - } - return out, false - - case reflect.Slice, reflect.Array: - if v.Len() == 0 && !forceKeep { - return nil, true - } - arr := make([]any, 0, v.Len()) - allZero := true - for i := 0; i < v.Len(); i++ { - // Build path for array element - elementPath := fmt.Sprintf("%s[%d]", currentPath, i) - if currentPath == "" { - elementPath = fmt.Sprintf("[%d]", i) - } - - elementForce := matchesExclusion(elementPath, exclusions) - val, zero := prune(v.Index(i), elementForce, exclusions, elementPath) - arr = append(arr, val) - if !zero { - allZero = false - } - } - if allZero && !forceKeep { - return nil, true - } - return arr, false - - case reflect.Map: - if v.Len() == 0 && !forceKeep { - return nil, true - } - m := make(map[string]any) - allZero := true - for _, k := range v.MapKeys() { - if k.Kind() != reflect.String { // JSON maps need string keys - continue - } - - // Build path for map key - keyPath := currentPath - if keyPath == "" { - keyPath = k.String() - } else { - keyPath = keyPath + "." + k.String() - } - - keyForce := matchesExclusion(keyPath, exclusions) - val, zero := prune(v.MapIndex(k), keyForce, exclusions, keyPath) - if zero && !keyForce { - continue - } - allZero = false - m[k.String()] = val - } - if allZero && !forceKeep { - return nil, true - } - return m, false - - case reflect.Bool: - if !v.Bool() && !forceKeep { - return nil, true - } - return v.Bool(), false - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if v.Int() == 0 && !forceKeep { - return nil, true - } - return v.Int(), false - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - if v.Uint() == 0 && !forceKeep { - return nil, true - } - return v.Uint(), false - case reflect.Float32, reflect.Float64: - if v.Float() == 0 && !forceKeep { - return nil, true - } - return v.Float(), false - case reflect.String: - if v.Len() == 0 && !forceKeep { - return nil, true - } - return v.String(), false - default: // chan, func, unsafe pointers, etc. - return v.Interface(), false - } -} - -// ---------------- tag parsing ---------------- - -func parseJSONTag(tag string) (name string, opts tagOpts) { - if idx := strings.Index(tag, ","); idx != -1 { - return tag[:idx], tagOpts(tag[idx+1:]) - } - return tag, tagOpts("") -} - -type tagOpts string - -func (o tagOpts) contains(opt string) bool { - if len(o) == 0 { - return false - } - s := string(o) - for s != "" { - var next string - i := strings.Index(s, ",") - if i >= 0 { - s, next = s[:i], s[i+1:] - } - if s == opt { - return true - } - s = next - } - return false -} - -// hasNoOmit returns true if either a dedicated no_omit tag exists -// OR the json tag contains ",no_omit". -func hasNoOmit(tag reflect.StructTag, jOpts tagOpts) bool { - if tag.Get("no_omit") != "" { - return true - } - return jOpts.contains("no_omit") -} diff --git a/aibridged/util/zero_marshaler_test.go b/aibridged/util/zero_marshaler_test.go deleted file mode 100644 index c388a22baef79..0000000000000 --- a/aibridged/util/zero_marshaler_test.go +++ /dev/null @@ -1,385 +0,0 @@ -package util - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" -) - -type TestStruct struct { - Name string `json:"name"` - Age int `json:"age"` - Profile *ProfileStruct `json:"profile"` - Items []string `json:"items"` - Meta map[string]string `json:"meta"` - Active bool `json:"active"` - Score float64 `json:"score"` -} - -type ProfileStruct struct { - Bio string `json:"bio"` - Website string `json:"website"` -} - -func TestMarshalNoZero_BasicFunctionality(t *testing.T) { - t.Parallel() - // Test basic zero omission without exclusions - data := TestStruct{ - Name: "John", - // Age: 0 (zero value, should be omitted) - // Profile: nil (zero value, should be omitted) - Items: []string{}, // empty slice (zero value, should be omitted) - // Meta: nil (zero value, should be omitted) - // Active: false (zero value, should be omitted) - // Score: 0.0 (zero value, should be omitted) - } - - result, err := MarshalNoZero(data) - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - // Should only contain non-zero fields - require.Equal(t, "John", unmarshaled["name"]) - require.NotContains(t, unmarshaled, "age") - require.NotContains(t, unmarshaled, "profile") - require.NotContains(t, unmarshaled, "items") - require.NotContains(t, unmarshaled, "meta") - require.NotContains(t, unmarshaled, "active") - require.NotContains(t, unmarshaled, "score") -} - -func TestMarshalNoZero_WithNoOmitTag(t *testing.T) { - t.Parallel() - type TestStructWithTag struct { - Name string `json:"name"` - Age int `json:"age,no_omit"` - } - - data := TestStructWithTag{ - Name: "John", - Age: 0, // Should be included due to no_omit tag - } - - result, err := MarshalNoZero(data) - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - require.Equal(t, "John", unmarshaled["name"]) - require.Equal(t, float64(0), unmarshaled["age"]) // JSON numbers are float64 -} - -func TestMarshalNoZero_WithJMESPathExclusions(t *testing.T) { - t.Parallel() - data := TestStruct{ - Name: "John", - Age: 0, // Should be included due to exclusion - Score: 0.0, // Should be included due to exclusion - Active: false, // Should be included due to exclusion - Profile: &ProfileStruct{ - Bio: "", // Should be included due to exclusion - Website: "", // Should be omitted (not in exclusion) - }, - Items: []string{}, // Should be included due to exclusion - Meta: map[string]string{}, // Should be omitted (not in exclusion) - } - - result, err := MarshalNoZero(data, "age", "score", "active", "profile.bio", "items") - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - require.Equal(t, "John", unmarshaled["name"]) - require.Equal(t, float64(0), unmarshaled["age"]) - require.Equal(t, float64(0), unmarshaled["score"]) - require.Equal(t, false, unmarshaled["active"]) - require.Contains(t, unmarshaled, "items") - require.NotContains(t, unmarshaled, "meta") - - // Check nested profile - profile := unmarshaled["profile"].(map[string]interface{}) - require.Equal(t, "", profile["bio"]) - require.NotContains(t, profile, "website") -} - -func TestMarshalNoZero_ArrayExclusions(t *testing.T) { - t.Parallel() - type ArrayTest struct { - Items []TestStruct `json:"items"` - } - - data := ArrayTest{ - Items: []TestStruct{ - {Name: "John", Age: 0}, // Age should be omitted normally - {Name: "", Age: 25}, // Name should be omitted normally - }, - } - - // Test excluding specific array elements - result, err := MarshalNoZero(data, "items[0].age", "items[1].name") - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - items := unmarshaled["items"].([]interface{}) - require.Len(t, items, 2) - - // First item should have age included - item0 := items[0].(map[string]interface{}) - require.Equal(t, "John", item0["name"]) - require.Equal(t, float64(0), item0["age"]) - - // Second item should have name included - item1 := items[1].(map[string]interface{}) - require.Equal(t, "", item1["name"]) - require.Equal(t, float64(25), item1["age"]) -} - -func TestMarshalNoZero_WildcardExclusions(t *testing.T) { - t.Parallel() - type WildcardTest struct { - Users []TestStruct `json:"users"` - } - - data := WildcardTest{ - Users: []TestStruct{ - {Name: "John", Age: 0}, - {Name: "Jane", Age: 0}, - }, - } - - // Test wildcard exclusion - result, err := MarshalNoZero(data, "users[*].age") - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - users := unmarshaled["users"].([]interface{}) - require.Len(t, users, 2) - - // Both users should have age included - user0 := users[0].(map[string]interface{}) - require.Equal(t, "John", user0["name"]) - require.Equal(t, float64(0), user0["age"]) - - user1 := users[1].(map[string]interface{}) - require.Equal(t, "Jane", user1["name"]) - require.Equal(t, float64(0), user1["age"]) -} - -func TestMarshalNoZero_MapExclusions(t *testing.T) { - t.Parallel() - data := TestStruct{ - Name: "John", - Meta: map[string]string{ - "key1": "", // Should be omitted normally - "key2": "value", - }, - } - - result, err := MarshalNoZero(data, "meta.key1") - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - require.Equal(t, "John", unmarshaled["name"]) - meta := unmarshaled["meta"].(map[string]interface{}) - require.Equal(t, "", meta["key1"]) // Should be included due to exclusion - require.Equal(t, "value", meta["key2"]) -} - -func TestMarshalNoZero_ComplexNestedExclusions(t *testing.T) { - t.Parallel() - type NestedTest struct { - Level1 struct { - Level2 struct { - Value string `json:"value"` - Count int `json:"count"` - } `json:"level2"` - Items []struct { - Name string `json:"name"` - ID int `json:"id"` - } `json:"items"` - } `json:"level1"` - } - - data := NestedTest{} - data.Level1.Level2.Value = "" // Should be included due to exclusion - data.Level1.Level2.Count = 0 // Should be omitted - data.Level1.Items = []struct { - Name string `json:"name"` - ID int `json:"id"` - }{ - {Name: "", ID: 0}, // Name should be included due to exclusion - } - - result, err := MarshalNoZero(data, "level1.level2.value", "level1.items[0].name") - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - level1 := unmarshaled["level1"].(map[string]interface{}) - level2 := level1["level2"].(map[string]interface{}) - require.Equal(t, "", level2["value"]) - require.NotContains(t, level2, "count") - - items := level1["items"].([]interface{}) - require.Len(t, items, 1) - item0 := items[0].(map[string]interface{}) - require.Equal(t, "", item0["name"]) - require.NotContains(t, item0, "id") -} - -func TestMarshalNoZero_EmptyExclusions(t *testing.T) { - t.Parallel() - data := TestStruct{ - Name: "John", - Age: 0, - } - - // Test with empty exclusions array - result, err := MarshalNoZero(data, []string{}...) - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - require.Equal(t, "John", unmarshaled["name"]) - require.NotContains(t, unmarshaled, "age") -} - -func TestMarshalNoZero_InvalidJMESPath(t *testing.T) { - t.Parallel() - data := TestStruct{ - Name: "John", - Age: 0, - } - - // Test with invalid JMESPath - should not crash, just ignore invalid patterns - result, err := MarshalNoZero(data, "invalid[[[path", "age") - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - require.Equal(t, "John", unmarshaled["name"]) - require.Equal(t, float64(0), unmarshaled["age"]) // Should still be included due to valid "age" exclusion -} - -func TestMarshalNoZero_BackwardCompatibility(t *testing.T) { - t.Parallel() - data := TestStruct{ - Name: "John", - Age: 0, - } - - // Test calling without any exclusions (backward compatibility) - result, err := MarshalNoZero(data) - require.NoError(t, err) - - var unmarshaled map[string]interface{} - err = json.Unmarshal(result, &unmarshaled) - require.NoError(t, err) - - require.Equal(t, "John", unmarshaled["name"]) - require.NotContains(t, unmarshaled, "age") -} - -func TestCreateTestObject(t *testing.T) { - t.Parallel() - tests := []struct { - path string - expected map[string]interface{} - hasError bool - }{ - { - path: "user.name", - expected: map[string]interface{}{ - "user": map[string]interface{}{ - "name": true, - }, - }, - hasError: false, - }, - { - path: "items[0].name", - expected: map[string]interface{}{ - "items": []interface{}{ - map[string]interface{}{ - "name": true, - }, - }, - }, - hasError: false, - }, - { - path: "simple", - expected: map[string]interface{}{ - "simple": true, - }, - hasError: false, - }, - { - path: "", - expected: nil, - hasError: true, - }, - } - - for _, test := range tests { - t.Run(test.path, func(t *testing.T) { - t.Parallel() - result, err := createTestObject(test.path) - if test.hasError { - require.Error(t, err) - require.Nil(t, result) - } else { - require.NoError(t, err) - require.Equal(t, test.expected, result) - } - }) - } -} - -func TestMatchesExclusion(t *testing.T) { - t.Parallel() - tests := []struct { - path string - exclusions []string - expected bool - }{ - {"user.name", []string{"user.name"}, true}, - {"user.age", []string{"user.name"}, false}, - {"items[0].name", []string{"items[0].name"}, true}, - {"items[1].name", []string{"items[0].name"}, false}, - {"user.profile.bio", []string{"user.profile.*"}, true}, - {"", []string{"user.name"}, false}, - {"user.name", []string{}, false}, - } - - for _, test := range tests { - t.Run(test.path, func(t *testing.T) { - t.Parallel() - result := matchesExclusion(test.path, test.exclusions) - require.Equal(t, test.expected, result) - }) - } -} From 9f32d26131463645e3947c84b379adefb1b92e78 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 23 Jul 2025 09:45:03 +0200 Subject: [PATCH 34/61] chore: inject mcp tools into openai reqs Signed-off-by: Danny Kopping --- aibridged/bridge.go | 134 ++++++++++++++++++++++++++++---------------- aibridged/mcp.go | 32 +++++------ 2 files changed, 101 insertions(+), 65 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 7ae0c9d1e2350..b8b9d23a5a5d7 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -23,6 +23,7 @@ import ( "github.com/mark3labs/mcp-go/mcp" "github.com/openai/openai-go" "github.com/openai/openai-go/shared/constant" + "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "cdr.dev/slog" @@ -105,53 +106,53 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli bridge.clientFn = clientFn bridge.logger = logger - // const ( - // githubMCPName = "github" - // coderMCPName = "coder" - // ) - // githubMCP, err := NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ - // "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), - // }, logger.Named("mcp-bridge-github")) - // if err != nil { - // return nil, xerrors.Errorf("github MCP bridge setup: %w", err) - // } - // coderMCP, err := NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ - // "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), - // // This is necessary to even access the MCP endpoint. - // "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), - // }, logger.Named("mcp-bridge-coder")) - // if err != nil { - // return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) - // } - - // bridge.mcpBridges = map[string]*MCPToolBridge{ - // githubMCPName: githubMCP, - // coderMCPName: coderMCP, - // } - - // ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - // defer cancel() - - // var eg errgroup.Group - // eg.Go(func() error { - // err := githubMCP.Init(ctx) - // if err == nil { - // return nil - // } - // return xerrors.Errorf("github: %w", err) - // }) - // eg.Go(func() error { - // err := coderMCP.Init(ctx) - // if err == nil { - // return nil - // } - // return xerrors.Errorf("coder: %w", err) - // }) - - // // This must block requests until MCP proxies are setup. - // if err := eg.Wait(); err != nil { - // return nil, xerrors.Errorf("MCP proxy init: %w", err) - // } + const ( + githubMCPName = "github" + coderMCPName = "coder" + ) + githubMCP, err := NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ + "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), + }, logger.Named("mcp-bridge-github")) + if err != nil { + return nil, xerrors.Errorf("github MCP bridge setup: %w", err) + } + coderMCP, err := NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ + "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), + // This is necessary to even access the MCP endpoint. + "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), + }, logger.Named("mcp-bridge-coder")) + if err != nil { + return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) + } + + bridge.mcpBridges = map[string]*MCPToolBridge{ + githubMCPName: githubMCP, + coderMCPName: coderMCP, + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + var eg errgroup.Group + eg.Go(func() error { + err := githubMCP.Init(ctx) + if err == nil { + return nil + } + return xerrors.Errorf("github: %w", err) + }) + eg.Go(func() error { + err := coderMCP.Init(ctx) + if err == nil { + return nil + } + return xerrors.Errorf("coder: %w", err) + }) + + // This must block requests until MCP proxies are setup. + if err := eg.Wait(); err != nil { + return nil, xerrors.Errorf("MCP proxy init: %w", err) + } return &bridge, nil } @@ -227,7 +228,30 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), }, in.Messages...) - // TODO: implement MCP tool injection. + for _, proxy := range b.mcpBridges { + for _, tool := range proxy.ListTools() { + fn := openai.ChatCompletionToolParam{ + Function: openai.FunctionDefinitionParam{ + Name: tool.Name, + Strict: openai.Bool(false), // TODO: configurable. + Description: openai.String(tool.Description), + Parameters: openai.FunctionParameters{ + "type": "object", + "properties": tool.Params, + // "additionalProperties": false, // Only relevant when strict=true. + }, + }, + } + + // Otherwise the request fails with "None is not of type 'array'" if a nil slice is given. + if len(tool.Required) > 0 { + // Must list ALL properties when strict=true. + fn.Function.Parameters["required"] = tool.Required + } + + in.Tools = append(in.Tools, fn) + } + } client := openai.NewClient() @@ -413,7 +437,19 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } for _, proxy := range b.mcpBridges { - in.Tools = append(in.Tools, proxy.ListTools()...) + for _, tool := range proxy.ListTools() { + in.Tools = append(in.Tools, anthropic.BetaToolUnionParam{ + OfTool: &anthropic.BetaToolParam{ + InputSchema: anthropic.BetaToolInputSchemaParam{ + Properties: tool.Params, + Required: tool.Required, + }, + Name: tool.Name, + Description: anthropic.String(tool.Description), + Type: anthropic.BetaToolTypeCustom, + }, + }) + } } // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). diff --git a/aibridged/mcp.go b/aibridged/mcp.go index 899472a2ac9cc..b54e6c59f6a6e 100644 --- a/aibridged/mcp.go +++ b/aibridged/mcp.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/anthropics/anthropic-sdk-go" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/client/transport" "github.com/mark3labs/mcp-go/mcp" @@ -18,7 +17,14 @@ type MCPToolBridge struct { name string client *client.Client logger slog.Logger - foundTools map[string]anthropic.BetaToolUnionParam + foundTools map[string]*MCPTool +} + +type MCPTool struct { + Name string + Description string + Params map[string]any + Required []string } const MCPProxyDelimiter = "_" @@ -61,7 +67,7 @@ func (b *MCPToolBridge) Init(ctx context.Context) error { return nil } -func (b *MCPToolBridge) ListTools() []anthropic.BetaToolUnionParam { +func (b *MCPToolBridge) ListTools() []*MCPTool { return maps.Values(b.foundTools) } @@ -83,7 +89,7 @@ func (b *MCPToolBridge) CallTool(ctx context.Context, name string, input any) (* }) } -func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]anthropic.BetaToolUnionParam, error) { +func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]*MCPTool, error) { initReq := mcp.InitializeRequest{ Params: mcp.InitializeParams{ ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, @@ -106,21 +112,15 @@ func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]anthropic return nil, xerrors.Errorf("list MCP tools: %w", err) } - out := make(map[string]anthropic.BetaToolUnionParam, len(tools.Tools)) + out := make(map[string]*MCPTool, len(tools.Tools)) for _, tool := range tools.Tools { - out[tool.Name] = anthropic.BetaToolUnionParam{ - OfTool: &anthropic.BetaToolParam{ - InputSchema: anthropic.BetaToolInputSchemaParam{ - Properties: tool.InputSchema.Properties, - Required: tool.InputSchema.Required, - }, - Name: fmt.Sprintf("%s%s%s", b.name, MCPProxyDelimiter, tool.Name), - Description: anthropic.String(tool.Description), - Type: anthropic.BetaToolTypeCustom, - }, + out[tool.Name] = &MCPTool{ + Name: fmt.Sprintf("%s%s%s", b.name, MCPProxyDelimiter, tool.Name), + Description: tool.Description, + Params: tool.InputSchema.Properties, + Required: tool.InputSchema.Required, } } - return out, nil } From 60b35713ef72c5fef6d3b9ef16e01b3748dbd985 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 23 Jul 2025 14:53:29 +0200 Subject: [PATCH 35/61] openai streaming implementation working with mcp tools Signed-off-by: Danny Kopping --- aibridged/bridge.go | 176 +++++++++++++++++++++++++++++++++++++------- aibridged/openai.go | 18 +++++ 2 files changed, 166 insertions(+), 28 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index b8b9d23a5a5d7..cae5d4fd1f323 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -22,6 +22,7 @@ import ( "github.com/google/uuid" "github.com/mark3labs/mcp-go/mcp" "github.com/openai/openai-go" + "github.com/openai/openai-go/packages/ssestream" "github.com/openai/openai-go/shared/constant" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" @@ -172,14 +173,14 @@ func (b *Bridge) openAITarget() *url.URL { // - https://platform.openai.com/docs/api-reference/chat-streaming func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { sessionID := uuid.New() - b.logger.Info(r.Context(), "OpenAI request started", slog.F("sessionID", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) + b.logger.Info(r.Context(), "openai request started", slog.F("session_id", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) // Clear any previous error state b.clearError() defer func() { - b.logger.Info(r.Context(), "OpenAI request ended", slog.F("sessionID", sessionID)) + b.logger.Info(r.Context(), "openai request ended", slog.F("session_id", sessionID)) _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) }() @@ -227,6 +228,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { in.Messages = append([]openai.ChatCompletionMessageParamUnion{ openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), }, in.Messages...) + in.StreamOptions.IncludeUsage = openai.Bool(true) for _, proxy := range b.mcpBridges { for _, tool := range proxy.ListTools() { @@ -253,7 +255,9 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } } + // client := openai.NewClient(oai_option.WithMiddleware(LoggingMiddleware)) client := openai.NewClient() + messages := in.ChatCompletionNewParams if in.Stream { streamCtx, streamCancel := context.WithCancelCause(ctx) @@ -268,35 +272,150 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { defer wg.Done() defer func() { if err := events.Close(streamCtx); err != nil { - b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("sessionID", sessionID)) + b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("session_id", sessionID)) } }() BasicSSESender(streamCtx, sessionID, "", events, b.logger.Named("sse-sender")).ServeHTTP(w, r) }() - stream := client.Chat.Completions.NewStreaming(ctx, in.ChatCompletionNewParams) + // TODO: implement parallel tool calls. + // TODO: don't send if not supported by model (i.e. o4-mini). + messages.ParallelToolCalls = openai.Bool(false) - var acc openai.ChatCompletionAccumulator - for stream.Next() { - chunk := stream.Current() - acc.AddChunk(chunk) - events.TrySend(ctx, chunk) - } + var ( + stream *ssestream.Stream[openai.ChatCompletionChunk] + cumulativeUsage openai.CompletionUsage + ) + for { + var pendingToolCalls []openai.FinishedChatCompletionToolCall + + stream = client.Chat.Completions.NewStreaming(ctx, messages) + var acc openai.ChatCompletionAccumulator + for stream.Next() { + chunk := stream.Current() + acc.AddChunk(chunk) + + fmt.Printf("[in]: %s\n", chunk.RawJSON()) + + shouldRelayChunk := true + if toolCall, ok := acc.JustFinishedToolCall(); ok { + // Don't intercept and handle builtin tools. + if b.isInjectedTool(toolCall.Name) { + pendingToolCalls = append(pendingToolCalls, toolCall) + // Don't relay this chunk back; we'll handle it transparently. + shouldRelayChunk = false + } + } - if err := events.Close(streamCtx); err != nil { - b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) - } + if len(pendingToolCalls) > 0 { + // Any chunks following a tool call invocation should not be relayed. + shouldRelayChunk = false + } - if err := stream.Err(); err != nil { - if isConnectionError(err) { - b.logger.Debug(ctx, "upstream connection closed", slog.Error(err)) - } else { - b.logger.Error(ctx, "server stream error", slog.Error(err)) - b.setError(err) + cumulativeUsage = sumUsage(cumulativeUsage, chunk.Usage) + + if shouldRelayChunk { + // If usage information is available, relay the cumulative usage once all tool invocations have completed. + if chunk.Usage.CompletionTokens > 0 { + chunk.Usage = cumulativeUsage + } + + events.TrySend(ctx, chunk) + + fmt.Printf("\t[out]: %s\n", chunk.RawJSON()) + } + } + + // If the usage information is set, track it. + // The API will send usage information when the response terminates, which will happen if a tool call is invoked. + if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + MsgId: acc.ID, + Model: string(acc.Model), + InputTokens: cumulativeUsage.PromptTokens, + OutputTokens: cumulativeUsage.CompletionTokens, + Other: map[string]int64{ + "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, + "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, + "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, + "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, + "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, + "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, + }, + }); err != nil { + b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) + } + + if err := stream.Err(); err != nil { + if isConnectionError(err) { + b.logger.Debug(ctx, "upstream connection closed", slog.Error(err)) + } else { + b.logger.Error(ctx, "server stream error", slog.Error(err)) + b.setError(err) + } + + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if len(pendingToolCalls) == 0 { + break } - http.Error(w, err.Error(), http.StatusInternalServerError) + appendedPrevMsg := false + for _, tc := range pendingToolCalls { + serverName, toolName, found := parseToolName(tc.Name) + if !found { + // Not an MCP proxy call, don't do anything. + continue + } + + // Only do this once. + if !appendedPrevMsg { + // Append the whole message from this stream as context since we'll be sending a new request with the tool results. + messages.Messages = append(messages.Messages, acc.Choices[len(acc.Choices)-1].Message.ToParam()) + appendedPrevMsg = true + } + + var ( + serialized map[string]string + buf bytes.Buffer + ) + _ = json.NewEncoder(&buf).Encode(tc.Arguments) + _ = json.NewDecoder(&buf).Decode(&serialized) + + res, err := b.mcpBridges[serverName].CallTool(streamCtx, toolName, serialized) + if err != nil { + // Always provide a tool_result even if the tool call failed + errorResponse := map[string]interface{}{ + "error": true, + "message": err.Error(), + } + errorJSON, _ := json.Marshal(errorResponse) + messages.Messages = append(messages.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + var out strings.Builder + if err := json.NewEncoder(&out).Encode(res); err != nil { + b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) + // Always provide a tool_result even if encoding failed + // TODO: abstract. + errorResponse := map[string]interface{}{ + "error": true, + "message": err.Error(), + } + errorJSON, _ := json.Marshal(errorResponse) + messages.Messages = append(messages.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + messages.Messages = append(messages.Messages, openai.ToolMessage(out.String(), tc.ID)) + } + } + + err = events.Close(streamCtx) + if err != nil { + b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) } wg.Wait() @@ -304,11 +423,15 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // Ensure we flush all the remaining data before ending. flush(w) - streamCancel(xerrors.New("gracefully done")) + if err != nil { + streamCancel(xerrors.Errorf("stream err: %w", err)) + } else { + streamCancel(xerrors.New("gracefully done")) + } <-streamCtx.Done() } else { - completion, err := client.Chat.Completions.New(ctx, in.ChatCompletionNewParams) + completion, err := client.Chat.Completions.New(ctx, messages) if err != nil { b.logger.Error(ctx, "chat completion failed", slog.Error(err)) b.setError(err) @@ -347,14 +470,14 @@ func LoggingMiddleware(req *http.Request, next option.MiddlewareNext) (res *http func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { sessionID := uuid.New() - b.logger.Info(r.Context(), "Anthropic request started", slog.F("sessionID", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) + b.logger.Info(r.Context(), "anthropic request started", slog.F("session_id", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) // Clear any previous error state b.clearError() defer func() { - b.logger.Info(r.Context(), "Anthropic request ended", slog.F("sessionID", sessionID)) + b.logger.Info(r.Context(), "anthropic request ended", slog.F("session_id", sessionID)) _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) }() @@ -595,7 +718,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { defer wg.Done() defer func() { if err := es.Close(streamCtx); err != nil { - b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("sessionID", sessionID)) + b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("session_id", sessionID)) } }() @@ -624,9 +747,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } // Tool-related handling. - - // TODO: this should *ignore* built-in tools; so ONLY do this for injected tooling. - switch event.Type { case string(constant.ValueOf[ant_constant.ContentBlockStart]()): // Have to do this because otherwise content_block_delta and content_block_start both match the type anthropic.BetaRawContentBlockStartEvent switch block := event.AsContentBlockStart().ContentBlock.AsAny().(type) { diff --git a/aibridged/openai.go b/aibridged/openai.go index aafcf95733376..0f3ef7d963535 100644 --- a/aibridged/openai.go +++ b/aibridged/openai.go @@ -41,3 +41,21 @@ func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { return nil } + +func sumUsage(ref, in openai.CompletionUsage) openai.CompletionUsage { + return openai.CompletionUsage{ + CompletionTokens: ref.CompletionTokens + in.CompletionTokens, + PromptTokens: ref.PromptTokens + in.PromptTokens, + TotalTokens: ref.TotalTokens + in.TotalTokens, + CompletionTokensDetails: openai.CompletionUsageCompletionTokensDetails{ + AcceptedPredictionTokens: ref.CompletionTokensDetails.AcceptedPredictionTokens + in.CompletionTokensDetails.AcceptedPredictionTokens, + AudioTokens: ref.CompletionTokensDetails.AudioTokens + in.CompletionTokensDetails.AudioTokens, + ReasoningTokens: ref.CompletionTokensDetails.ReasoningTokens + in.CompletionTokensDetails.ReasoningTokens, + RejectedPredictionTokens: ref.CompletionTokensDetails.RejectedPredictionTokens + in.CompletionTokensDetails.RejectedPredictionTokens, + }, + PromptTokensDetails: openai.CompletionUsagePromptTokensDetails{ + AudioTokens: ref.PromptTokensDetails.AudioTokens + in.PromptTokensDetails.AudioTokens, + CachedTokens: ref.PromptTokensDetails.CachedTokens + in.PromptTokensDetails.CachedTokens, + }, + } +} From e368454802e324d0995815d441efc307200e66aa Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 23 Jul 2025 17:30:24 +0200 Subject: [PATCH 36/61] non-streaming mode for openai Signed-off-by: Danny Kopping --- aibridged/bridge.go | 148 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 130 insertions(+), 18 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index cae5d4fd1f323..43a51fc8149a6 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -228,7 +228,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { in.Messages = append([]openai.ChatCompletionMessageParamUnion{ openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), }, in.Messages...) - in.StreamOptions.IncludeUsage = openai.Bool(true) for _, proxy := range b.mcpBridges { for _, tool := range proxy.ListTools() { @@ -257,9 +256,11 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // client := openai.NewClient(oai_option.WithMiddleware(LoggingMiddleware)) client := openai.NewClient() - messages := in.ChatCompletionNewParams + req := in.ChatCompletionNewParams if in.Stream { + in.StreamOptions.IncludeUsage = openai.Bool(true) + streamCtx, streamCancel := context.WithCancelCause(ctx) defer streamCancel(xerrors.New("deferred")) @@ -281,7 +282,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // TODO: implement parallel tool calls. // TODO: don't send if not supported by model (i.e. o4-mini). - messages.ParallelToolCalls = openai.Bool(false) + req.ParallelToolCalls = openai.Bool(false) var ( stream *ssestream.Stream[openai.ChatCompletionChunk] @@ -290,7 +291,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { for { var pendingToolCalls []openai.FinishedChatCompletionToolCall - stream = client.Chat.Completions.NewStreaming(ctx, messages) + stream = client.Chat.Completions.NewStreaming(ctx, req) var acc openai.ChatCompletionAccumulator for stream.Next() { chunk := stream.Current() @@ -347,7 +348,11 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } if err := stream.Err(); err != nil { - if isConnectionError(err) { + var apierr *openai.Error + if errors.As(err, &apierr) { + http.Error(w, apierr.Message, apierr.StatusCode) + return + } else if isConnectionError(err) { b.logger.Debug(ctx, "upstream connection closed", slog.Error(err)) } else { b.logger.Error(ctx, "server stream error", slog.Error(err)) @@ -372,7 +377,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // Only do this once. if !appendedPrevMsg { // Append the whole message from this stream as context since we'll be sending a new request with the tool results. - messages.Messages = append(messages.Messages, acc.Choices[len(acc.Choices)-1].Message.ToParam()) + req.Messages = append(req.Messages, acc.Choices[len(acc.Choices)-1].Message.ToParam()) appendedPrevMsg = true } @@ -391,7 +396,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { "message": err.Error(), } errorJSON, _ := json.Marshal(errorResponse) - messages.Messages = append(messages.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) continue } @@ -405,11 +410,11 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { "message": err.Error(), } errorJSON, _ := json.Marshal(errorResponse) - messages.Messages = append(messages.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) continue } - messages.Messages = append(messages.Messages, openai.ToolMessage(out.String(), tc.ID)) + req.Messages = append(req.Messages, openai.ToolMessage(out.String(), tc.ID)) } } @@ -431,16 +436,122 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { <-streamCtx.Done() } else { - completion, err := client.Chat.Completions.New(ctx, messages) - if err != nil { - b.logger.Error(ctx, "chat completion failed", slog.Error(err)) - b.setError(err) - http.Error(w, "chat completion failed", http.StatusInternalServerError) - return - } + // Non-streaming case with tool calling support + var cumulativeUsage openai.CompletionUsage + + for { + completion, err := client.Chat.Completions.New(ctx, req) + if err != nil { + if isConnectionError(err) { + b.logger.Debug(ctx, "upstream connection closed", slog.Error(err)) + return // Don't send error response if client already disconnected + } + var apierr *openai.Error + if errors.As(err, &apierr) { + http.Error(w, apierr.Message, apierr.StatusCode) + return + } - w.WriteHeader(http.StatusOK) // TODO: always? - _, _ = w.Write([]byte(completion.RawJSON())) + b.logger.Error(ctx, "chat completion failed", slog.Error(err)) + b.setError(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Track cumulative usage + cumulativeUsage = sumUsage(cumulativeUsage, completion.Usage) + + // Track token usage + if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + MsgId: completion.ID, + Model: string(completion.Model), + InputTokens: cumulativeUsage.PromptTokens, + OutputTokens: cumulativeUsage.CompletionTokens, + Other: map[string]int64{ + "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, + "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, + "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, + "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, + "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, + "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, + }, + }); err != nil { + b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) + } + + // Check if we have tool calls to process + var pendingToolCalls []openai.ChatCompletionMessageToolCall + if len(completion.Choices) > 0 && completion.Choices[0].Message.ToolCalls != nil { + for _, toolCall := range completion.Choices[0].Message.ToolCalls { + if b.isInjectedTool(toolCall.Function.Name) { + pendingToolCalls = append(pendingToolCalls, toolCall) + } + } + } + + // If no injected tool calls, we're done + if len(pendingToolCalls) == 0 { + // Update the cumulative usage in the final response + if completion.Usage.CompletionTokens > 0 { + completion.Usage = cumulativeUsage + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(completion.RawJSON())) + break + } + + appendedPrevMsg := false + // Process each pending tool call + for _, tc := range pendingToolCalls { + serverName, toolName, found := parseToolName(tc.Function.Name) + if !found { + // Not an MCP proxy call, don't do anything. + continue + } + // Only do this once. + if !appendedPrevMsg { + // Append the whole message from this stream as context since we'll be sending a new request with the tool results. + req.Messages = append(req.Messages, completion.Choices[0].Message.ToParam()) + appendedPrevMsg = true + } + + var ( + serialized map[string]string + buf bytes.Buffer + ) + _ = json.NewEncoder(&buf).Encode(tc.Function.Arguments) + _ = json.NewDecoder(&buf).Decode(&serialized) + + res, err := b.mcpBridges[serverName].CallTool(ctx, toolName, serialized) + if err != nil { + // Always provide a tool result even if the tool call failed + errorResponse := map[string]interface{}{ + "error": true, + "message": err.Error(), + } + errorJSON, _ := json.Marshal(errorResponse) + req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + var out strings.Builder + if err := json.NewEncoder(&out).Encode(res); err != nil { + b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) + // Always provide a tool result even if encoding failed + errorResponse := map[string]interface{}{ + "error": true, + "message": err.Error(), + } + errorJSON, _ := json.Marshal(errorResponse) + req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + req.Messages = append(req.Messages, openai.ToolMessage(out.String(), tc.ID)) + } + } } } @@ -468,6 +579,7 @@ func LoggingMiddleware(req *http.Request, next option.MiddlewareNext) (res *http return res, err } +// TODO: track cumulative usage when tool invocations are executed; see OpenAI implementation. func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { sessionID := uuid.New() b.logger.Info(r.Context(), "anthropic request started", slog.F("session_id", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) From c3d986a50db45f6e19719d8cbd7e8e796b0c955e Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 24 Jul 2025 11:49:24 +0200 Subject: [PATCH 37/61] openai test Signed-off-by: Danny Kopping --- aibridged/bridge.go | 242 ++++++------ aibridged/bridge_test.go | 345 ++++++++++++------ .../anthropic/single_builtin_tool.txtar | 4 +- .../fixtures/openai/single_builtin_tool.txtar | 102 ++++++ aibridged/proto/aibridged.pb.go | 107 +++--- aibridged/proto/aibridged.proto | 2 +- codersdk/deployment.go | 24 +- 7 files changed, 551 insertions(+), 275 deletions(-) create mode 100644 aibridged/fixtures/openai/single_builtin_tool.txtar diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 43a51fc8149a6..49b104d798f96 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -22,9 +22,9 @@ import ( "github.com/google/uuid" "github.com/mark3labs/mcp-go/mcp" "github.com/openai/openai-go" + oai_option "github.com/openai/openai-go/option" "github.com/openai/openai-go/packages/ssestream" "github.com/openai/openai-go/shared/constant" - "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "cdr.dev/slog" @@ -107,59 +107,59 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli bridge.clientFn = clientFn bridge.logger = logger - const ( - githubMCPName = "github" - coderMCPName = "coder" - ) - githubMCP, err := NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ - "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), - }, logger.Named("mcp-bridge-github")) - if err != nil { - return nil, xerrors.Errorf("github MCP bridge setup: %w", err) - } - coderMCP, err := NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ - "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), - // This is necessary to even access the MCP endpoint. - "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), - }, logger.Named("mcp-bridge-coder")) - if err != nil { - return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) - } - - bridge.mcpBridges = map[string]*MCPToolBridge{ - githubMCPName: githubMCP, - coderMCPName: coderMCP, - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - var eg errgroup.Group - eg.Go(func() error { - err := githubMCP.Init(ctx) - if err == nil { - return nil - } - return xerrors.Errorf("github: %w", err) - }) - eg.Go(func() error { - err := coderMCP.Init(ctx) - if err == nil { - return nil - } - return xerrors.Errorf("coder: %w", err) - }) - - // This must block requests until MCP proxies are setup. - if err := eg.Wait(); err != nil { - return nil, xerrors.Errorf("MCP proxy init: %w", err) - } + // const ( + // githubMCPName = "github" + // coderMCPName = "coder" + // ) + // githubMCP, err := NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ + // "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), + // }, logger.Named("mcp-bridge-github")) + // if err != nil { + // return nil, xerrors.Errorf("github MCP bridge setup: %w", err) + // } + // coderMCP, err := NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ + // "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), + // // This is necessary to even access the MCP endpoint. + // "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), + // }, logger.Named("mcp-bridge-coder")) + // if err != nil { + // return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) + // } + + // bridge.mcpBridges = map[string]*MCPToolBridge{ + // githubMCPName: githubMCP, + // coderMCPName: coderMCP, + // } + + // ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + // defer cancel() + + // var eg errgroup.Group + // eg.Go(func() error { + // err := githubMCP.Init(ctx) + // if err == nil { + // return nil + // } + // return xerrors.Errorf("github: %w", err) + // }) + // eg.Go(func() error { + // err := coderMCP.Init(ctx) + // if err == nil { + // return nil + // } + // return xerrors.Errorf("coder: %w", err) + // }) + + // // This must block requests until MCP proxies are setup. + // if err := eg.Wait(); err != nil { + // return nil, xerrors.Errorf("MCP proxy init: %w", err) + // } return &bridge, nil } func (b *Bridge) openAITarget() *url.URL { - u := b.cfg.OpenAIBaseURL.String() + u := b.cfg.OpenAI.BaseURL.String() target, err := url.Parse(u) if err != nil { panic(fmt.Sprintf("failed to parse %q", u)) @@ -254,8 +254,16 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } } - // client := openai.NewClient(oai_option.WithMiddleware(LoggingMiddleware)) - client := openai.NewClient() + // Configure OpenAI client with authentication + var opts []oai_option.RequestOption + if apiKey := b.cfg.OpenAI.Key.String(); apiKey != "" { + opts = append(opts, oai_option.WithAPIKey(apiKey)) + } + if baseURL := b.cfg.OpenAI.BaseURL.String(); baseURL != "" { + opts = append(opts, oai_option.WithBaseURL(baseURL)) + } + + client := openai.NewClient(opts...) req := in.ChatCompletionNewParams if in.Stream { @@ -306,6 +314,15 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { pendingToolCalls = append(pendingToolCalls, toolCall) // Don't relay this chunk back; we'll handle it transparently. shouldRelayChunk = false + } else { + _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + Model: string(in.Model), + Input: toolCall.Arguments, + Tool: toolCall.Name, + }) + if err != nil { + b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) + } } } @@ -381,14 +398,24 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { appendedPrevMsg = true } + _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + Model: string(in.Model), + Input: tc.Arguments, + Tool: tc.Name, + Injected: true, + }) + if err != nil { + b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + } + var ( - serialized map[string]string - buf bytes.Buffer + args map[string]any + buf bytes.Buffer ) _ = json.NewEncoder(&buf).Encode(tc.Arguments) - _ = json.NewDecoder(&buf).Decode(&serialized) + _ = json.NewDecoder(&buf).Decode(&args) - res, err := b.mcpBridges[serverName].CallTool(streamCtx, toolName, serialized) + res, err := b.mcpBridges[serverName].CallTool(streamCtx, toolName, args) if err != nil { // Always provide a tool_result even if the tool call failed errorResponse := map[string]interface{}{ @@ -485,6 +512,15 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { for _, toolCall := range completion.Choices[0].Message.ToolCalls { if b.isInjectedTool(toolCall.Function.Name) { pendingToolCalls = append(pendingToolCalls, toolCall) + } else { + _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + Model: string(in.Model), + Input: toolCall.Function.Arguments, + Tool: toolCall.Function.Name, + }) + if err != nil { + b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) + } } } } @@ -517,14 +553,23 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { appendedPrevMsg = true } + _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + Model: string(in.Model), + Input: tc.Function.Arguments, + Tool: tc.Function.Name, + Injected: true, + }) + if err != nil { + b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + } + var ( - serialized map[string]string - buf bytes.Buffer + args map[string]string + buf bytes.Buffer ) _ = json.NewEncoder(&buf).Encode(tc.Function.Arguments) - _ = json.NewDecoder(&buf).Decode(&serialized) - - res, err := b.mcpBridges[serverName].CallTool(ctx, toolName, serialized) + _ = json.NewDecoder(&buf).Decode(&args) + res, err := b.mcpBridges[serverName].CallTool(ctx, toolName, args) if err != nil { // Always provide a tool result even if the tool call failed errorResponse := map[string]interface{}{ @@ -780,20 +825,18 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } if input != nil { - var ( - serialized map[string]string - buf bytes.Buffer - ) - _ = json.NewEncoder(&buf).Encode(input) - _ = json.NewDecoder(&buf).Decode(&serialized) - - _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - Model: string(resp.Model), - Input: serialized, - Tool: toolUse.Name, - }) - if err != nil { - b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + if serialized, err := json.Marshal(input); err == nil { + _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + Model: string(resp.Model), + Input: string(serialized), + Tool: toolUse.Name, + Injected: b.isInjectedTool(toolUse.Name), + }) + if err != nil { + b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + } + } else { + b.logger.Warn(ctx, "failed to marshal args for tool usage", slog.Error(err)) } } @@ -979,23 +1022,20 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { continue } - var ( - serialized map[string]string - buf bytes.Buffer - ) - _ = json.NewEncoder(&buf).Encode(input) - _ = json.NewDecoder(&buf).Decode(&serialized) - fmt.Printf("[event] %s\n[tool(%q)] %s %+v\n\n", event.RawJSON(), id, name, input) - _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ - Model: string(message.Model), - Input: serialized, - Tool: toolName, - Injected: true, - }) - if err != nil { - b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + if serialized, err := json.Marshal(input); err == nil { + _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ + Model: string(message.Model), + Input: string(serialized), + Tool: toolName, + Injected: true, + }) + if err != nil { + b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + } + } else { + b.logger.Warn(ctx, "failed to marshal args for tool usage", slog.Error(err)) } res, err := b.mcpBridges[serverName].CallTool(streamCtx, toolName, input) @@ -1132,19 +1172,17 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { continue } - var ( - serialized map[string]string - buf bytes.Buffer - ) - _ = json.NewEncoder(&buf).Encode(variant.Input) - _ = json.NewDecoder(&buf).Decode(&serialized) - _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ - Model: string(message.Model), - Input: serialized, - Tool: variant.Name, - }) - if err != nil { - b.logger.Error(ctx, "failed to track non-injected tool usage", slog.Error(err)) + if serialized, err := json.Marshal(variant.Input); err == nil { + _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ + Model: string(message.Model), + Input: string(serialized), + Tool: variant.Name, + }) + if err != nil { + b.logger.Error(ctx, "failed to track non-injected tool usage", slog.Error(err)) + } + } else { + b.logger.Warn(ctx, "failed to marshal args for tool usage", slog.Error(err)) } } } diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index 7a69a256398ef..03796e6eea8d7 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -5,7 +5,9 @@ import ( "bytes" "context" _ "embed" + "encoding/json" "fmt" + "io" "net" "net/http" "net/http/httptest" @@ -17,6 +19,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" "github.com/coder/coder/v2/aibridged" "github.com/coder/coder/v2/aibridged/proto" @@ -30,12 +33,15 @@ import ( var ( //go:embed fixtures/anthropic/single_builtin_tool.txtar antSingleBuiltinTool []byte + + //go:embed fixtures/openai/single_builtin_tool.txtar + oaiSingleBuiltinTool []byte ) const ( - FixtureRequest = "request" - FixtureStreamingResponse = "streaming" - FixtureNonStreamingResponse = "non-streaming" + fixtureRequest = "request" + fixtureStreamingResponse = "streaming" + fixtureNonStreamingResponse = "non-streaming" ) func TestAnthropicMessages(t *testing.T) { @@ -49,89 +55,198 @@ func TestAnthropicMessages(t *testing.T) { cases := []struct { streaming bool }{ - // { - // streaming: true, - // }, + { + streaming: true, + }, { streaming: false, }, } for _, tc := range cases { - arc := txtar.Parse(antSingleBuiltinTool) - t.Logf("%s: %s", t.Name(), arc.Comment) - - files := filesMap(arc) - require.Len(t, files, 3) - require.Contains(t, files, FixtureRequest) - require.Contains(t, files, FixtureStreamingResponse) - require.Contains(t, files, FixtureNonStreamingResponse) - - // Replace macro to indicate whether request is streaming or not. - reqBody := files[FixtureRequest] - require.Contains(t, string(reqBody), "%STREAMING%", "missing %STREAMING% macro in request") - reqBody = bytes.Replace(reqBody, []byte("%STREAMING%"), fmt.Appendf(nil, "%v", tc.streaming), 1) - - ctx := testutil.Context(t, testutil.WaitLong) - srv := newMockServer(ctx, files) - t.Cleanup(srv.Close) - - coderdClient := &fakeBridgeDaemonClient{} - - logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) - b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - Anthropic: codersdk.AIBridgeAnthropicConfig{ - BaseURL: serpent.String(srv.URL), - Key: serpent.String(sessionToken), - }, - }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return coderdClient, true + t.Run(fmt.Sprintf("%s/streaming=%v", t.Name(), tc.streaming), func(t *testing.T) { + t.Parallel() + + arc := txtar.Parse(antSingleBuiltinTool) + t.Logf("%s: %s", t.Name(), arc.Comment) + + files := filesMap(arc) + require.Len(t, files, 3) + require.Contains(t, files, fixtureRequest) + require.Contains(t, files, fixtureStreamingResponse) + require.Contains(t, files, fixtureNonStreamingResponse) + + reqBody := files[fixtureRequest] + + // Add the stream param to the request. + newBody, err := sjson.SetBytes(reqBody, "stream", tc.streaming) + require.NoError(t, err) + reqBody = newBody + + ctx := testutil.Context(t, testutil.WaitLong) + srv := newMockServer(ctx, t, files) + t.Cleanup(srv.Close) + + coderdClient := &fakeBridgeDaemonClient{} + + logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + Anthropic: codersdk.AIBridgeAnthropicConfig{ + BaseURL: serpent.String(srv.URL), + Key: serpent.String(sessionToken), + }, + }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return coderdClient, true + }) + require.NoError(t, err) + + go func() { + assert.NoError(t, b.Serve()) + }() + // Wait for bridge to come up. + require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) + + // Make API call to aibridge for Anthropic /v1/messages + req := createAnthropicMessagesReq(t, "http://"+b.Addr(), reqBody) + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() + + // Response-specific checks. + if tc.streaming { + sp := aibridged.NewSSEParser() + require.NoError(t, sp.Parse(resp.Body)) + + // Ensure the message starts and completes, at a minimum. + assert.Contains(t, sp.AllEvents(), "message_start") + assert.Contains(t, sp.AllEvents(), "message_stop") + require.Len(t, coderdClient.tokenUsages, 2) // One from message_start, one from message_delta. + } else { + require.Len(t, coderdClient.tokenUsages, 1) + } + + assert.NotZero(t, calculateTotalInputTokens(coderdClient.tokenUsages)) + assert.NotZero(t, calculateTotalOutputTokens(coderdClient.tokenUsages)) + + var args map[string]any + require.NoError(t, json.Unmarshal([]byte(coderdClient.toolUsages[0].Input), &args)) + + require.Len(t, coderdClient.toolUsages, 1) + assert.Equal(t, "Read", coderdClient.toolUsages[0].Tool) + require.Contains(t, args, "file_path") + assert.Equal(t, "/tmp/blah/foo", args["file_path"]) + + require.Len(t, coderdClient.userPrompts, 1) + assert.Equal(t, "read the foo file", coderdClient.userPrompts[0].Prompt) }) - require.NoError(t, err) - - go func() { - assert.NoError(t, b.Serve()) - }() - // Wait for bridge to come up. - require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) - - // Make API call to aibridge for Anthropic /v1/messages - req := createAnthropicMessagesReq(t, "http://"+b.Addr(), reqBody) - if tc.streaming { - req.Header.Set("Accept", "text/event-stream") - } else { - req.Header.Set("Accept", "application/json") - } - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - defer resp.Body.Close() - - // Response-specific checks. - if tc.streaming { - sp := aibridged.NewSSEParser() - require.NoError(t, sp.Parse(resp.Body)) - - // Ensure the message starts and completes, at a minimum. - assert.Contains(t, sp.AllEvents(), "message_start") - assert.Contains(t, sp.AllEvents(), "message_stop") - require.Len(t, coderdClient.tokenUsages, 2) // One from message_start, one from message_delta. - } else { + } + }) +} + +func TestOpenAIChatCompletions(t *testing.T) { + t.Parallel() + + sessionToken := getSessionToken(t) + + t.Run("single builtin tool", func(t *testing.T) { + t.Parallel() + + cases := []struct { + streaming bool + }{ + { + streaming: true, + }, + { + streaming: false, + }, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("%s/streaming=%v", t.Name(), tc.streaming), func(t *testing.T) { + t.Parallel() + + arc := txtar.Parse(oaiSingleBuiltinTool) + t.Logf("%s: %s", t.Name(), arc.Comment) + + files := filesMap(arc) + require.Len(t, files, 3) + require.Contains(t, files, fixtureRequest) + require.Contains(t, files, fixtureStreamingResponse) + require.Contains(t, files, fixtureNonStreamingResponse) + + reqBody := files[fixtureRequest] + + // Add the stream param to the request. + newBody, err := sjson.SetBytes(reqBody, "stream", tc.streaming) + require.NoError(t, err) + reqBody = newBody + + ctx := testutil.Context(t, testutil.WaitLong) + srv := newMockServer(ctx, t, files) + t.Cleanup(srv.Close) + + coderdClient := &fakeBridgeDaemonClient{} + + logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + OpenAI: codersdk.AIBridgeOpenAIConfig{ + BaseURL: serpent.String(srv.URL), + Key: serpent.String(sessionToken), + }, + }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return coderdClient, true + }) + require.NoError(t, err) + + go func() { + assert.NoError(t, b.Serve()) + }() + // Wait for bridge to come up. + require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) + + // Make API call to aibridge for OpenAI /v1/chat/completions + req := createOpenAIChatCompletionsReq(t, "http://"+b.Addr(), reqBody) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() + + // Response-specific checks. + if tc.streaming { + sp := aibridged.NewSSEParser() + require.NoError(t, sp.Parse(resp.Body)) + + // OpenAI sends all events under the same type. + messageEvents := sp.MessageEvents() + assert.NotEmpty(t, messageEvents) + + // OpenAI streaming ends with [DONE] + lastEvent := messageEvents[len(messageEvents)-1] + assert.Equal(t, "[DONE]", lastEvent.Data) + } + require.Len(t, coderdClient.tokenUsages, 1) - } + assert.NotZero(t, calculateTotalInputTokens(coderdClient.tokenUsages)) + assert.NotZero(t, calculateTotalOutputTokens(coderdClient.tokenUsages)) - assert.NotZero(t, calculateTotalInputTokens(coderdClient.tokenUsages)) - assert.NotZero(t, calculateTotalOutputTokens(coderdClient.tokenUsages)) + var args map[string]any + require.NoError(t, json.Unmarshal([]byte(coderdClient.toolUsages[0].Input), &args)) - require.Len(t, coderdClient.toolUsages, 1) - assert.Equal(t, "Read", coderdClient.toolUsages[0].Tool) - require.Contains(t, coderdClient.toolUsages[0].Input, "file_path") - assert.Equal(t, "/tmp/blah/foo", coderdClient.toolUsages[0].Input["file_path"]) + require.Len(t, coderdClient.toolUsages, 1) + assert.Equal(t, "read_file", coderdClient.toolUsages[0].Tool) + require.Contains(t, args, "path") + assert.Equal(t, "README.md", args["path"]) - require.Len(t, coderdClient.userPrompts, 1) - assert.Equal(t, "read the foo file", coderdClient.userPrompts[0].Prompt) + require.Len(t, coderdClient.userPrompts, 1) + assert.Equal(t, "how large is the README.md file in my current path", coderdClient.userPrompts[0].Prompt) + }) } }) } @@ -176,6 +291,16 @@ func createAnthropicMessagesReq(t *testing.T, baseURL string, input []byte) *htt return req } +func createOpenAIChatCompletionsReq(t *testing.T, baseURL string, input []byte) *http.Request { + t.Helper() + + req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/v1/chat/completions", bytes.NewReader(input)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + return req +} + func getSessionToken(t *testing.T) string { t.Helper() @@ -194,42 +319,48 @@ type mockServer struct { *httptest.Server } -func newMockServer(ctx context.Context, files archiveFileMap) *mockServer { +func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap) *mockServer { + t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - contentType := r.Header.Get("Accept") - switch contentType { - // SSE - case "text/event-stream": - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") - - scanner := bufio.NewScanner(bytes.NewReader(files[FixtureStreamingResponse])) - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "Streaming unsupported", http.StatusInternalServerError) - return - } - - for scanner.Scan() { - line := scanner.Text() - - fmt.Fprintf(w, "%s\n", line) - flusher.Flush() - } - - if err := scanner.Err(); err != nil { - http.Error(w, fmt.Sprintf("Error reading fixture: %v", err), http.StatusInternalServerError) - return - } - case "application/json": + body, err := io.ReadAll(r.Body) + defer r.Body.Close() + require.NoError(t, err) + + type msg struct { + Stream bool `json:"stream"` + } + var reqMsg msg + require.NoError(t, json.Unmarshal(body, &reqMsg)) + + if !reqMsg.Stream { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(files[FixtureNonStreamingResponse]) + w.Write(files[fixtureNonStreamingResponse]) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + scanner := bufio.NewScanner(bytes.NewReader(files[fixtureStreamingResponse])) + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + return + } + + for scanner.Scan() { + line := scanner.Text() + + fmt.Fprintf(w, "%s\n", line) + flusher.Flush() + } - default: - panic(fmt.Sprintf("unsupported content type: %q", contentType)) + if err := scanner.Err(); err != nil { + http.Error(w, fmt.Sprintf("Error reading fixture: %v", err), http.StatusInternalServerError) + return } })) diff --git a/aibridged/fixtures/anthropic/single_builtin_tool.txtar b/aibridged/fixtures/anthropic/single_builtin_tool.txtar index 55f7f043d9ac0..e095649cae18f 100644 --- a/aibridged/fixtures/anthropic/single_builtin_tool.txtar +++ b/aibridged/fixtures/anthropic/single_builtin_tool.txtar @@ -9,8 +9,7 @@ Claude Code has builtin tools to (e.g.) explore the filesystem. "role": "user", "content": "read the foo file" } - ], - "stream": %STREAMING% + ] } -- streaming -- @@ -126,4 +125,3 @@ data: {"type":"message_stop" } } } ---- diff --git a/aibridged/fixtures/openai/single_builtin_tool.txtar b/aibridged/fixtures/openai/single_builtin_tool.txtar new file mode 100644 index 0000000000000..0eae82126a0e2 --- /dev/null +++ b/aibridged/fixtures/openai/single_builtin_tool.txtar @@ -0,0 +1,102 @@ +LLM (https://llm.datasette.io/) configured with a simple "read_file" tool. + +-- request -- +{ + "messages": [ + { + "role": "user", + "content": "how large is the README.md file in my current path" + } + ], + "model": "gpt-4.1", + "tools": [ + { + "type": "function", + "function": { + "name": "read_file", + "description": "Read the contents of a file at the given path.", + "parameters": { + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + } + } + ] +} + +-- streaming -- +data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_HjeqP7YeRkoNj0de9e3U4X4B","type":"function","function":{"name":"read_file","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"path"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"README"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":".md"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} + +data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[],"usage":{"prompt_tokens":60,"completion_tokens":15,"total_tokens":75,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + +data: [DONE] + +-- non-streaming -- +{ + "id": "chatcmpl-BwkyFElDIr1egmFyfQ9z4vPBto7m2", + "object": "chat.completion", + "created": 1753343279, + "model": "gpt-4.1-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_KjzAbhiZC6nk81tQzL7pwlpc", + "type": "function", + "function": { + "name": "read_file", + "arguments": "{\"path\":\"README.md\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 60, + "completion_tokens": 15, + "total_tokens": 75, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_b3f1157249" +} + diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index dcdff3425d84f..e3044adc59898 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -235,10 +235,10 @@ type TrackToolUsageRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Tool string `protobuf:"bytes,1,opt,name=tool,proto3" json:"tool,omitempty"` - Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` - Input map[string]string `protobuf:"bytes,3,rep,name=input,proto3" json:"input,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - Injected bool `protobuf:"varint,4,opt,name=injected,proto3" json:"injected,omitempty"` + Tool string `protobuf:"bytes,1,opt,name=tool,proto3" json:"tool,omitempty"` + Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` + Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + Injected bool `protobuf:"varint,4,opt,name=injected,proto3" json:"injected,omitempty"` } func (x *TrackToolUsageRequest) Reset() { @@ -287,11 +287,11 @@ func (x *TrackToolUsageRequest) GetModel() string { return "" } -func (x *TrackToolUsageRequest) GetInput() map[string]string { +func (x *TrackToolUsageRequest) GetInput() string { if x != nil { return x.Input } - return nil + return "" } func (x *TrackToolUsageRequest) GetInjected() bool { @@ -369,44 +369,37 @@ var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xda, 0x01, 0x0a, 0x15, 0x54, 0x72, 0x61, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x73, 0x0a, 0x15, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x18, 0x0a, + 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x9b, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, + 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, + 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, + 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, + 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, + 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, + 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, + 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, + 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, + 0x0a, 0x0e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x20, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x41, 0x0a, 0x05, - 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x69, - 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, - 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6e, - 0x70, 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, - 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x1a, 0x38, 0x0a, 0x0a, 0x49, - 0x6e, 0x70, 0x75, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, - 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, - 0x9b, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, - 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, - 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, - 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, - 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, - 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, - 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, - 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x69, 0x62, - 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, - 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, - 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, + 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -421,7 +414,7 @@ func file_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { return file_aibridged_proto_aibridged_proto_rawDescData } -var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*TrackTokenUsageRequest)(nil), // 0: aibridged.TrackTokenUsageRequest (*TrackTokenUsageResponse)(nil), // 1: aibridged.TrackTokenUsageResponse @@ -430,22 +423,20 @@ var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*TrackToolUsageRequest)(nil), // 4: aibridged.TrackToolUsageRequest (*TrackToolUsageResponse)(nil), // 5: aibridged.TrackToolUsageResponse nil, // 6: aibridged.TrackTokenUsageRequest.OtherEntry - nil, // 7: aibridged.TrackToolUsageRequest.InputEntry } var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ 6, // 0: aibridged.TrackTokenUsageRequest.other:type_name -> aibridged.TrackTokenUsageRequest.OtherEntry - 7, // 1: aibridged.TrackToolUsageRequest.input:type_name -> aibridged.TrackToolUsageRequest.InputEntry - 0, // 2: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest - 2, // 3: aibridged.AIBridgeDaemon.TrackUserPrompt:input_type -> aibridged.TrackUserPromptRequest - 4, // 4: aibridged.AIBridgeDaemon.TrackToolUsage:input_type -> aibridged.TrackToolUsageRequest - 1, // 5: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse - 3, // 6: aibridged.AIBridgeDaemon.TrackUserPrompt:output_type -> aibridged.TrackUserPromptResponse - 5, // 7: aibridged.AIBridgeDaemon.TrackToolUsage:output_type -> aibridged.TrackToolUsageResponse - 5, // [5:8] is the sub-list for method output_type - 2, // [2:5] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 0, // 1: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest + 2, // 2: aibridged.AIBridgeDaemon.TrackUserPrompt:input_type -> aibridged.TrackUserPromptRequest + 4, // 3: aibridged.AIBridgeDaemon.TrackToolUsage:input_type -> aibridged.TrackToolUsageRequest + 1, // 4: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse + 3, // 5: aibridged.AIBridgeDaemon.TrackUserPrompt:output_type -> aibridged.TrackUserPromptResponse + 5, // 6: aibridged.AIBridgeDaemon.TrackToolUsage:output_type -> aibridged.TrackToolUsageResponse + 4, // [4:7] is the sub-list for method output_type + 1, // [1:4] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_aibridged_proto_aibridged_proto_init() } @@ -533,7 +524,7 @@ func file_aibridged_proto_aibridged_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_aibridged_proto_aibridged_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index 64fac90b2b256..324b3427d88e4 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -21,7 +21,7 @@ message TrackUserPromptResponse {} message TrackToolUsageRequest { string tool = 1; string model = 2; - map input = 3; + string input = 3; bool injected = 4; } message TrackToolUsageResponse {} diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 732db7199b5b4..ace0170be3608 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3228,12 +3228,23 @@ Write out the current server config as YAML to stdout.`, Description: "TODO.", Flag: "ai-bridge-openai-base-url", Env: "CODER_AI_BRIDGE_OPENAI_BASE_URL", - Value: &c.AI.BridgeConfig.OpenAIBaseURL, + Value: &c.AI.BridgeConfig.OpenAI.BaseURL, Default: "https://api.openai.com", Group: &deploymentGroupAIBridge, YAML: "openai_base_url", Hidden: true, }, + { + Name: "AI Bridge OpenAI Key", + Description: "TODO.", + Flag: "ai-bridge-openai-key", + Env: "CODER_AI_BRIDGE_OPENAI_KEY", + Value: &c.AI.BridgeConfig.OpenAI.Key, + Default: "", + Group: &deploymentGroupAIBridge, + YAML: "openai_key", + Hidden: true, + }, { Name: "AI Bridge Anthropic Base URL", Description: "TODO.", @@ -3262,9 +3273,14 @@ Write out the current server config as YAML to stdout.`, } type AIBridgeConfig struct { - Daemons serpent.Int64 `json:"daemons" typescript:",notnull"` - OpenAIBaseURL serpent.String `json:"openai_base_url" typescript:",notnull"` - Anthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"` + Daemons serpent.Int64 `json:"daemons" typescript:",notnull"` + OpenAI AIBridgeOpenAIConfig `json:"openai" typescript:",notnull"` + Anthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"` +} + +type AIBridgeOpenAIConfig struct { + BaseURL serpent.String `json:"base_url" typescript:",notnull"` + Key serpent.String `json:"key" typescript:",notnull"` } type AIBridgeAnthropicConfig struct { From 40e1aa5787659f355544f4559d9c55b5e65f85b0 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 24 Jul 2025 11:58:10 +0200 Subject: [PATCH 38/61] calculating token totals Signed-off-by: Danny Kopping --- aibridged/bridge_test.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index 03796e6eea8d7..f1f3a8461a1b0 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -53,13 +53,18 @@ func TestAnthropicMessages(t *testing.T) { t.Parallel() cases := []struct { - streaming bool + streaming bool + expectedInputTokens, expectedOutputTokens int }{ { - streaming: true, + streaming: true, + expectedInputTokens: 2, + expectedOutputTokens: 66, }, { - streaming: false, + streaming: false, + expectedInputTokens: 5, + expectedOutputTokens: 84, }, } @@ -128,8 +133,8 @@ func TestAnthropicMessages(t *testing.T) { require.Len(t, coderdClient.tokenUsages, 1) } - assert.NotZero(t, calculateTotalInputTokens(coderdClient.tokenUsages)) - assert.NotZero(t, calculateTotalOutputTokens(coderdClient.tokenUsages)) + assert.EqualValues(t, tc.expectedInputTokens, calculateTotalInputTokens(coderdClient.tokenUsages), "input tokens miscalculated") + assert.EqualValues(t, tc.expectedOutputTokens, calculateTotalOutputTokens(coderdClient.tokenUsages), "output tokens miscalculated") var args map[string]any require.NoError(t, json.Unmarshal([]byte(coderdClient.toolUsages[0].Input), &args)) @@ -155,13 +160,18 @@ func TestOpenAIChatCompletions(t *testing.T) { t.Parallel() cases := []struct { - streaming bool + streaming bool + expectedInputTokens, expectedOutputTokens int }{ { - streaming: true, + streaming: true, + expectedInputTokens: 60, + expectedOutputTokens: 15, }, { - streaming: false, + streaming: false, + expectedInputTokens: 60, + expectedOutputTokens: 15, }, } @@ -233,8 +243,8 @@ func TestOpenAIChatCompletions(t *testing.T) { } require.Len(t, coderdClient.tokenUsages, 1) - assert.NotZero(t, calculateTotalInputTokens(coderdClient.tokenUsages)) - assert.NotZero(t, calculateTotalOutputTokens(coderdClient.tokenUsages)) + assert.EqualValues(t, tc.expectedInputTokens, calculateTotalInputTokens(coderdClient.tokenUsages), "input tokens miscalculated") + assert.EqualValues(t, tc.expectedOutputTokens, calculateTotalOutputTokens(coderdClient.tokenUsages), "output tokens miscalculated") var args map[string]any require.NoError(t, json.Unmarshal([]byte(coderdClient.toolUsages[0].Input), &args)) @@ -254,7 +264,7 @@ func TestOpenAIChatCompletions(t *testing.T) { func calculateTotalOutputTokens(in []*proto.TrackTokenUsageRequest) int64 { var total int64 for _, el := range in { - total += el.InputTokens + total += el.OutputTokens } return total } @@ -262,7 +272,7 @@ func calculateTotalOutputTokens(in []*proto.TrackTokenUsageRequest) int64 { func calculateTotalInputTokens(in []*proto.TrackTokenUsageRequest) int64 { var total int64 for _, el := range in { - total += el.OutputTokens + total += el.InputTokens } return total } From 4b65f129a59d92b761ba11938fb662cc31116060 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 24 Jul 2025 14:14:21 +0200 Subject: [PATCH 39/61] add simple test Signed-off-by: Danny Kopping --- aibridged/bridge_test.go | 122 +++++ aibridged/fixtures/anthropic/simple.txtar | 139 ++++++ aibridged/fixtures/openai/simple.txtar | 536 ++++++++++++++++++++++ 3 files changed, 797 insertions(+) create mode 100644 aibridged/fixtures/anthropic/simple.txtar create mode 100644 aibridged/fixtures/openai/simple.txtar diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index f1f3a8461a1b0..b1b73f86c7791 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -36,6 +36,12 @@ var ( //go:embed fixtures/openai/single_builtin_tool.txtar oaiSingleBuiltinTool []byte + + //go:embed fixtures/anthropic/simple.txtar + antSimple []byte + + //go:embed fixtures/openai/simple.txtar + oaiSimple []byte ) const ( @@ -261,6 +267,122 @@ func TestOpenAIChatCompletions(t *testing.T) { }) } +func TestSimple(t *testing.T) { + t.Parallel() + + sessionToken := getSessionToken(t) + + testCases := []struct { + name string + fixture []byte + configureFunc func(string, proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) + createRequest func(*testing.T, string, []byte) *http.Request + }{ + { + name: "anthropic", + fixture: antSimple, + configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { + logger := testutil.Logger(t) + return aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + Anthropic: codersdk.AIBridgeAnthropicConfig{ + BaseURL: serpent.String(addr), + Key: serpent.String(sessionToken), + }, + }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return client, true + }) + }, + createRequest: createAnthropicMessagesReq, + }, + { + name: "openai", + fixture: oaiSimple, + configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { + logger := testutil.Logger(t) + return aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + OpenAI: codersdk.AIBridgeOpenAIConfig{ + BaseURL: serpent.String(addr), + Key: serpent.String(sessionToken), + }, + }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return client, true + }) + }, + createRequest: createOpenAIChatCompletionsReq, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + streamingCases := []struct { + streaming bool + }{ + {streaming: true}, + {streaming: false}, + } + + for _, sc := range streamingCases { + t.Run(fmt.Sprintf("streaming=%v", sc.streaming), func(t *testing.T) { + t.Parallel() + + arc := txtar.Parse(tc.fixture) + t.Logf("%s: %s", t.Name(), arc.Comment) + + files := filesMap(arc) + require.Len(t, files, 3) + require.Contains(t, files, fixtureRequest) + require.Contains(t, files, fixtureStreamingResponse) + require.Contains(t, files, fixtureNonStreamingResponse) + + reqBody := files[fixtureRequest] + + // Add the stream param to the request. + newBody, err := sjson.SetBytes(reqBody, "stream", sc.streaming) + require.NoError(t, err) + reqBody = newBody + + // Given: a mock API server and a Bridge through which the requests will flow. + ctx := testutil.Context(t, testutil.WaitLong) + srv := newMockServer(ctx, t, files) + t.Cleanup(srv.Close) + + coderdClient := &fakeBridgeDaemonClient{} + + b, err := tc.configureFunc(srv.URL, coderdClient) + require.NoError(t, err) + + go func() { + assert.NoError(t, b.Serve()) + }() + // Wait for bridge to come up. + require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) + + // When: calling the "API server" with the fixture's request body. + req := tc.createRequest(t, "http://"+b.Addr(), reqBody) + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() + + // Then: I expect a non-empty response. + bodyBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.NotEmpty(t, bodyBytes, "should have received response body") + + // Then: I expect the prompt to have been tracked. + require.NotEmpty(t, coderdClient.userPrompts, "no prompts tracked") + assert.Equal(t, "how many angels can dance on the head of a pin", coderdClient.userPrompts[0].Prompt) + }) + } + }) + } +} + func calculateTotalOutputTokens(in []*proto.TrackTokenUsageRequest) int64 { var total int64 for _, el := range in { diff --git a/aibridged/fixtures/anthropic/simple.txtar b/aibridged/fixtures/anthropic/simple.txtar new file mode 100644 index 0000000000000..9cf6084fe40d2 --- /dev/null +++ b/aibridged/fixtures/anthropic/simple.txtar @@ -0,0 +1,139 @@ +Simple request. + +-- request -- +{ + "max_tokens": 8192, + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "how many angels can dance on the head of a pin\n" + } + ] + } + ], + "model": "claude-sonnet-4-0", + "temperature": 1 +} + +-- streaming -- +event: message_start +data: {"type":"message_start","message":{"id":"msg_01FeMXMphebFfFsTWMNHex7P","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} } + +event: content_block_start +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + +event: ping +data: {"type": "ping"} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"This"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" is a famous philosophical question often used to illustrate medieval"}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" scholastic debates that seem pointless or ov"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"erly abstract. The question \"How many angels can dance on the head of"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" a pin?\" is typically cited as an example of us"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"eless speculation.\n\nHistorically, medieval theolog"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ians did debate the nature of angels -"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" whether they were incorporeal beings, how"}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" they occupied space, and whether multiple angels could exist"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" in the same location. However, there"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'s little evidence they literally"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" debated dancing angels on pinheads.\n\nThe question has"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" no factual answer since it depends on assumptions about:"}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- The existence and nature of angels\n- Whether"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" incorporeal beings occupy physical space\n- What"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" constitutes \"dancing\" for a spiritual"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" entity\n- The size of both the"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" pin and the angels\n\nIt's become a metaph"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"or for overthinking trivial matters"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" or getting lost in theoretical discussions disconnected from practical reality."} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Some use it to critique certain types of academic"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" or theological debate, while others defen"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d the value of exploring fundamental questions about existence an"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d metaphysics.\n\nSo while u"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"nanswerable literally, it serves as an interesting lens"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for discussing the nature of philosophical inquiry itself."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":240} } + +event: message_stop +data: {"type":"message_stop" } + +-- non-streaming -- +{ + "id": "msg_01Pvyf26bY17RcjmWfJsXGBn", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-20250514", + "content": [ + { + "type": "text", + "text": "This is a famous philosophical question, often called \"How many angels can dance on the head of a pin?\" It's typically used to represent pointless or overly abstract theological debates.\n\nThe question doesn't have a literal answer because:\n\n1. **Historical context**: It's often attributed to medieval scholastic philosophers, though there's little evidence they actually debated this exact question. It became a popular way to mock what some saw as useless academic arguments.\n\n2. **Philosophical purpose**: The question highlights the difficulty of discussing non-physical beings (angels) in physical terms (space on a pinhead).\n\n3. **Different interpretations**: \n - If angels are purely spiritual, they might not take up physical space at all\n - If they do occupy space, we'd need to know their \"size\"\n - The question might be asking about the nature of space, matter, and spirit\n\nSo the real answer is that it's not meant to be answered literally - it's a thought experiment about the limits of rational inquiry and the sometimes absurd directions theological speculation can take.\n\nWould you like to explore the philosophical implications behind this question, or were you thinking about it in a different context?" + } + ], + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 18, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 254, + "service_tier": "standard" + } +} diff --git a/aibridged/fixtures/openai/simple.txtar b/aibridged/fixtures/openai/simple.txtar new file mode 100644 index 0000000000000..55cb3b91fea58 --- /dev/null +++ b/aibridged/fixtures/openai/simple.txtar @@ -0,0 +1,536 @@ +Simple request. + +-- request -- +{ + "messages": [ + { + "role": "user", + "content": "how many angels can dance on the head of a pin\n" + } + ], + "model": "gpt-4.1" +} + +-- streaming -- +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"How"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" many"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" angels"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" dance"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" on"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" head"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" pin"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"?\""},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" classic"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" example"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ph"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ilos"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"oph"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ical"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theological"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" r"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"iddle"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**,"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" not"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" genuine"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" inquiry"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" metaph"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ysical"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" realities"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" The"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" phrase"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" most"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" likely"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" originated"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" during"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"med"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ieval"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" schol"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"astic"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debates"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**,"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" where"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" scholars"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" engaged"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" complex"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" discussions"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" nature"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" spiritual"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" beings"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" limits"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" human"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" knowledge"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Meaning"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Context"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Not"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" meant"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" have"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" literal"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" answer"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Angels"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Christian"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theology"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" are"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" spiritual"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" ("},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"not"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" physical"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":")"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" beings"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" so"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" they"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" don"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"’t"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" occupy"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" space"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" physical"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" sense"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Symbol"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ic"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" purpose"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" The"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" often"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" used"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" mock"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" illustrate"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" arguments"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" perceived"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" overly"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" speculative"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" irrelevant"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Answers"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\""},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" through"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" History"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Sch"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ol"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ast"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ics"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" There's"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" little"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" evidence"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" medieval"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" scholars"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" literally"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debated"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" this"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":";"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" it's"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" later"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"car"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ic"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ature"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" their"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" intricate"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theological"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" arguments"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Modern"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" usage"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" It's"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" cited"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" an"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" example"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" pointless"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" un"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"answer"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"able"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Summary"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"There"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" no"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" specific"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" number"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":";"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" rhetorical"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" highlighting"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" limits"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theoretical"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" speculative"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" reasoning"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Would"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" like"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" know"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" medieval"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" schol"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"astic"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debates"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" how"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" this"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" used"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" modern"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" discourse"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + +data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":19,"completion_tokens":238,"total_tokens":257,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + +data: [DONE] + +-- non-streaming -- +{ + "id": "chatcmpl-BwojtGkriucZoAj99ylsoapwHo5V2", + "object": "chat.completion", + "created": 1753357765, + "model": "gpt-4.1-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The question \"How many angels can dance on the head of a pin?\" is a classic example of a rhetorical or philosophical question—*not* a real theological inquiry.\n\n**Origin and Meaning:**\n- The phrase is used to lampoon or satirize overly subtle, speculative, or irrelevant philosophical debates, especially those attributed to medieval scholasticism.\n- There is **no actual historical record** of medieval theologians debating this specific question.\n- It **illustrates debates about the nature of angels**—whether they occupy physical space, for example—but not in such literal terms.\n\n**If answered literally:**\n- If angels are considered non-corporeal and not limited by physical space, **an infinite number** could \"dance\" on the head of a pin.\n- If taken as a joke, the answer is up to the storyteller!\n\n**In summary:** \nIt's a facetious question highlighting the limits or absurdities of some philosophical or theological arguments. There is no fixed answer.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 19, + "completion_tokens": 200, + "total_tokens": 219, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_b3f1157249" +} + From 081013894cf877aabdd74f0253574aaca6a00839 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 24 Jul 2025 17:01:00 +0200 Subject: [PATCH 40/61] refactor mcp tool injection Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 4 +- aibridged/bridge.go | 184 +++++++++++++-------------------------- aibridged/bridge_test.go | 8 +- aibridged/mcp.go | 39 ++++++++- aibridged/streaming.go | 6 ++ cli/server.go | 58 +++++++++++- coderd/coderd.go | 54 ------------ codersdk/deployment.go | 6 +- 8 files changed, 170 insertions(+), 189 deletions(-) diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 47f3d9e76d19a..873b2a6da85ba 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -54,7 +54,7 @@ type Server struct { var _ proto.DRPCAIBridgeDaemonServer = &Server{} -func New(rpcDialer Dialer, httpAddr string, logger slog.Logger, bridgeCfg codersdk.AIBridgeConfig) (*Server, error) { +func New(rpcDialer Dialer, httpAddr string, logger slog.Logger, bridgeCfg codersdk.AIBridgeConfig, tools []*MCPTool) (*Server, error) { if rpcDialer == nil { return nil, xerrors.Errorf("nil rpcDialer given") } @@ -73,7 +73,7 @@ func New(rpcDialer Dialer, httpAddr string, logger slog.Logger, bridgeCfg coders // TODO: improve error handling here; if this fails it prevents the whole server from starting up! - bridge, err := NewBridge(bridgeCfg, httpAddr, logger.Named("ai_bridge"), daemon.client) + bridge, err := NewBridge(bridgeCfg, httpAddr, logger.Named("ai_bridge"), daemon.client, tools) if err != nil { return nil, xerrors.Errorf("create new bridge server: %w", err) } diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 49b104d798f96..8162dcde6b4ba 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -80,11 +80,10 @@ type Bridge struct { clientFn func() (proto.DRPCAIBridgeDaemonClient, bool) logger slog.Logger - lastErr error - mcpBridges map[string]*MCPToolBridge + tools map[string]*MCPTool } -func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool)) (*Bridge, error) { +func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool), tools []*MCPTool) (*Bridge, error) { var bridge Bridge mux := &http.ServeMux{} @@ -107,53 +106,10 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli bridge.clientFn = clientFn bridge.logger = logger - // const ( - // githubMCPName = "github" - // coderMCPName = "coder" - // ) - // githubMCP, err := NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ - // "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), - // }, logger.Named("mcp-bridge-github")) - // if err != nil { - // return nil, xerrors.Errorf("github MCP bridge setup: %w", err) - // } - // coderMCP, err := NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ - // "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), - // // This is necessary to even access the MCP endpoint. - // "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), - // }, logger.Named("mcp-bridge-coder")) - // if err != nil { - // return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) - // } - - // bridge.mcpBridges = map[string]*MCPToolBridge{ - // githubMCPName: githubMCP, - // coderMCPName: coderMCP, - // } - - // ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - // defer cancel() - - // var eg errgroup.Group - // eg.Go(func() error { - // err := githubMCP.Init(ctx) - // if err == nil { - // return nil - // } - // return xerrors.Errorf("github: %w", err) - // }) - // eg.Go(func() error { - // err := coderMCP.Init(ctx) - // if err == nil { - // return nil - // } - // return xerrors.Errorf("coder: %w", err) - // }) - - // // This must block requests until MCP proxies are setup. - // if err := eg.Wait(); err != nil { - // return nil, xerrors.Errorf("MCP proxy init: %w", err) - // } + bridge.tools = make(map[string]*MCPTool, len(tools)) + for _, tool := range tools { + bridge.tools[tool.ID] = tool + } return &bridge, nil } @@ -229,36 +185,36 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), }, in.Messages...) - for _, proxy := range b.mcpBridges { - for _, tool := range proxy.ListTools() { - fn := openai.ChatCompletionToolParam{ - Function: openai.FunctionDefinitionParam{ - Name: tool.Name, - Strict: openai.Bool(false), // TODO: configurable. - Description: openai.String(tool.Description), - Parameters: openai.FunctionParameters{ - "type": "object", - "properties": tool.Params, - // "additionalProperties": false, // Only relevant when strict=true. - }, + for _, tool := range b.tools { + fn := openai.ChatCompletionToolParam{ + Function: openai.FunctionDefinitionParam{ + Name: tool.ID, + Strict: openai.Bool(false), // TODO: configurable. + Description: openai.String(tool.Description), + Parameters: openai.FunctionParameters{ + "type": "object", + "properties": tool.Params, + // "additionalProperties": false, // Only relevant when strict=true. }, - } - - // Otherwise the request fails with "None is not of type 'array'" if a nil slice is given. - if len(tool.Required) > 0 { - // Must list ALL properties when strict=true. - fn.Function.Parameters["required"] = tool.Required - } + }, + } - in.Tools = append(in.Tools, fn) + // Otherwise the request fails with "None is not of type 'array'" if a nil slice is given. + if len(tool.Required) > 0 { + // Must list ALL properties when strict=true. + fn.Function.Parameters["required"] = tool.Required } + + in.Tools = append(in.Tools, fn) } // Configure OpenAI client with authentication var opts []oai_option.RequestOption - if apiKey := b.cfg.OpenAI.Key.String(); apiKey != "" { - opts = append(opts, oai_option.WithAPIKey(apiKey)) + apiKey := b.cfg.OpenAI.Key.String() + if apiKey == "" { + apiKey = os.Getenv("OPENAI_API_KEY") } + opts = append(opts, oai_option.WithAPIKey(apiKey)) if baseURL := b.cfg.OpenAI.BaseURL.String(); baseURL != "" { opts = append(opts, oai_option.WithBaseURL(baseURL)) } @@ -385,8 +341,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { appendedPrevMsg := false for _, tc := range pendingToolCalls { - serverName, toolName, found := parseToolName(tc.Name) - if !found { + if !b.isInjectedTool(tc.Name) { // Not an MCP proxy call, don't do anything. continue } @@ -408,14 +363,12 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) } - var ( - args map[string]any - buf bytes.Buffer - ) - _ = json.NewEncoder(&buf).Encode(tc.Arguments) - _ = json.NewDecoder(&buf).Decode(&args) + var args map[string]any + if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil { + b.logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", tc.Name)) + } - res, err := b.mcpBridges[serverName].CallTool(streamCtx, toolName, args) + res, err := b.tools[tc.Name].Call(streamCtx, args) if err != nil { // Always provide a tool_result even if the tool call failed errorResponse := map[string]interface{}{ @@ -541,8 +494,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { appendedPrevMsg := false // Process each pending tool call for _, tc := range pendingToolCalls { - serverName, toolName, found := parseToolName(tc.Function.Name) - if !found { + if !b.isInjectedTool(tc.ID) { // Not an MCP proxy call, don't do anything. continue } @@ -569,7 +521,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { ) _ = json.NewEncoder(&buf).Encode(tc.Function.Arguments) _ = json.NewDecoder(&buf).Decode(&args) - res, err := b.mcpBridges[serverName].CallTool(ctx, toolName, args) + res, err := b.tools[tc.ID].Call(ctx, args) if err != nil { // Always provide a tool result even if the tool call failed errorResponse := map[string]interface{}{ @@ -716,20 +668,18 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } - for _, proxy := range b.mcpBridges { - for _, tool := range proxy.ListTools() { - in.Tools = append(in.Tools, anthropic.BetaToolUnionParam{ - OfTool: &anthropic.BetaToolParam{ - InputSchema: anthropic.BetaToolInputSchemaParam{ - Properties: tool.Params, - Required: tool.Required, - }, - Name: tool.Name, - Description: anthropic.String(tool.Description), - Type: anthropic.BetaToolTypeCustom, + for _, tool := range b.tools { + in.Tools = append(in.Tools, anthropic.BetaToolUnionParam{ + OfTool: &anthropic.BetaToolParam{ + InputSchema: anthropic.BetaToolInputSchemaParam{ + Properties: tool.Params, + Required: tool.Required, }, - }) - } + Name: tool.ID, + Description: anthropic.String(tool.Description), + Type: anthropic.BetaToolTypeCustom, + }, + }) } // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). @@ -764,10 +714,12 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } opts = append(opts, option.WithMiddleware(LoggingMiddleware)) - // Lib will automatically look for ANTHROPIC_API_KEY env. - if _, ok := os.LookupEnv("ANTHROPIC_API_KEY"); !ok { - opts = append(opts, option.WithAPIKey(b.cfg.Anthropic.Key.String())) + apiKey := b.cfg.Anthropic.Key.String() + if apiKey == "" { + apiKey = os.Getenv("ANTHROPIC_API_KEY") } + + opts = append(opts, option.WithAPIKey(apiKey)) opts = append(opts, option.WithBaseURL(b.cfg.Anthropic.BaseURL.String())) // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") @@ -784,7 +736,8 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { b.logger.Error(ctx, "anthropic stream error", slog.Error(err)) if antErr := getAnthropicErrorResponse(err); antErr != nil { - fmt.Println("oops") + http.Error(w, antErr.Error.Message, antErr.StatusCode) + return } } @@ -995,12 +948,13 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { messages.Messages = append(messages.Messages, message.ToParam()) for name, id := range pendingToolCalls { - serverName, toolName, found := parseToolName(name) - if !found { + if !b.isInjectedTool(name) { // Not an MCP proxy call, don't do anything. continue } + tool := b.tools[name] + var ( input any foundTool bool @@ -1028,7 +982,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ Model: string(message.Model), Input: string(serialized), - Tool: toolName, + Tool: tool.Name, Injected: true, }) if err != nil { @@ -1038,7 +992,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { b.logger.Warn(ctx, "failed to marshal args for tool usage", slog.Error(err)) } - res, err := b.mcpBridges[serverName].CallTool(streamCtx, toolName, input) + res, err := b.tools[tool.ID].Call(streamCtx, input) if err != nil { // Always provide a tool_result even if the tool call failed messages.Messages = append(messages.Messages, @@ -1250,23 +1204,9 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } -func parseToolName(name string) (string, string, bool) { - serverName, toolName, found := strings.Cut(name, MCPProxyDelimiter) - return serverName, toolName, found -} - -func (b *Bridge) isInjectedTool(name string) bool { - serverName, toolName, found := parseToolName(name) - if !found { - return false - } - - mcp, ok := b.mcpBridges[serverName] - if !ok { - return false - } - - return mcp.HasTool(toolName) +func (b *Bridge) isInjectedTool(id string) bool { + _, ok := b.tools[id] + return ok } func getAnthropicErrorResponse(err error) *AnthropicErrorResponse { diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index b1b73f86c7791..a5b06478d7e3b 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -109,7 +109,7 @@ func TestAnthropicMessages(t *testing.T) { }, }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { return coderdClient, true - }) + }, nil) require.NoError(t, err) go func() { @@ -216,7 +216,7 @@ func TestOpenAIChatCompletions(t *testing.T) { }, }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { return coderdClient, true - }) + }, nil) require.NoError(t, err) go func() { @@ -291,7 +291,7 @@ func TestSimple(t *testing.T) { }, }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { return client, true - }) + }, nil) }, createRequest: createAnthropicMessagesReq, }, @@ -308,7 +308,7 @@ func TestSimple(t *testing.T) { }, }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { return client, true - }) + }, nil) }, createRequest: createOpenAIChatCompletionsReq, }, diff --git a/aibridged/mcp.go b/aibridged/mcp.go index b54e6c59f6a6e..966b8aeb21d96 100644 --- a/aibridged/mcp.go +++ b/aibridged/mcp.go @@ -3,6 +3,7 @@ package aibridged import ( "context" "fmt" + "strings" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/client/transport" @@ -21,13 +22,18 @@ type MCPToolBridge struct { } type MCPTool struct { + client *client.Client + ID string Name string Description string Params map[string]any Required []string } -const MCPProxyDelimiter = "_" +const ( + MCPPrefix = "__mcp__" + MCPDelimiter = "_" // TODO: ensure server names CANNOT contain this char. +) func NewMCPToolBridge(name, serverURL string, headers map[string]string, logger slog.Logger) (*MCPToolBridge, error) { // ts := transport.NewMemoryTokenStore() @@ -89,6 +95,15 @@ func (b *MCPToolBridge) CallTool(ctx context.Context, name string, input any) (* }) } +func (t *MCPTool) Call(ctx context.Context, input any) (*mcp.CallToolResult, error) { + return t.client.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: t.Name, + Arguments: input, + }, + }) +} + func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]*MCPTool, error) { initReq := mcp.InitializeRequest{ Params: mcp.InitializeParams{ @@ -115,7 +130,9 @@ func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]*MCPTool, out := make(map[string]*MCPTool, len(tools.Tools)) for _, tool := range tools.Tools { out[tool.Name] = &MCPTool{ - Name: fmt.Sprintf("%s%s%s", b.name, MCPProxyDelimiter, tool.Name), + client: b.client, + ID: EncodeToolID(b.name, tool.Name), + Name: tool.Name, Description: tool.Description, Params: tool.InputSchema.Properties, Required: tool.InputSchema.Required, @@ -124,6 +141,24 @@ func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]*MCPTool, return out, nil } +func EncodeToolID(server, tool string) string { + return fmt.Sprintf("%s%s%s%s", MCPPrefix, server, MCPDelimiter, tool) +} + +func DecodeToolID(id string) (string, string, error) { + _, name, ok := strings.Cut(id, MCPPrefix) + if !ok { + return "", "", xerrors.Errorf("unable to decode %q, prefix %q not found", id, MCPPrefix) + } + + server, tool, ok := strings.Cut(name, MCPDelimiter) + if !ok { + return "", "", xerrors.Errorf("unable to decode %q, delimiter %q not found", id, MCPDelimiter) + } + + return server, tool, nil +} + func (b *MCPToolBridge) Close() { // TODO: atomically close. } diff --git a/aibridged/streaming.go b/aibridged/streaming.go index c7f6b496c6328..abb4336ef270c 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -92,6 +92,12 @@ func flush(w http.ResponseWriter) { return } + defer func() { + if r := recover(); r != nil { + // Silently handle panic from flush, likely due to broken connection + } + }() + flusher.Flush() } diff --git a/cli/server.go b/cli/server.go index 721f437234e1d..2665d5792082d 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1572,13 +1572,67 @@ func newProvisionerDaemon( func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string, bridgeCfg codersdk.AIBridgeConfig) (*aibridged.Server, error) { httpAddr := "0.0.0.0:0" // TODO: configurable. - + // TODO: in reality, it won't work this way. We'll have to load the tools dynamically + tools, err := loadMCP(coderAPI.Logger.Named("mcp-tools")) + if err != nil { + coderAPI.Logger.Error(ctx, "failed to load MCP tools", slog.Error(err)) + } return aibridged.New(func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { // This debounces calls to listen every second. // TODO: is this true / necessary? return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx, name) - }, httpAddr, coderAPI.Logger.Named("aibridged").With(slog.F("name", name)), bridgeCfg) + }, httpAddr, coderAPI.Logger.Named("aibridged").With(slog.F("name", name)), bridgeCfg, tools) +} + +func loadMCP(logger slog.Logger) ([]*aibridged.MCPTool, error) { + const ( + githubMCPName = "github" + coderMCPName = "coder" + ) + githubMCP, err := aibridged.NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ + "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), + }, logger.Named("mcp-bridge-github")) + if err != nil { + return nil, xerrors.Errorf("github MCP bridge setup: %w", err) + } + coderMCP, err := aibridged.NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ + // "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), + // This is necessary to even access the MCP endpoint. + "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), + }, logger.Named("mcp-bridge-coder")) + if err != nil { + return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) + } + + var eg errgroup.Group + eg.Go(func() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + err := githubMCP.Init(ctx) + if err == nil { + return nil + } + return xerrors.Errorf("github: %w", err) + }) + eg.Go(func() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + err := coderMCP.Init(ctx) + if err == nil { + return nil + } + return xerrors.Errorf("coder: %w", err) + }) + + // This must block requests until MCP proxies are setup. + if err := eg.Wait(); err != nil { + return nil, xerrors.Errorf("MCP proxy init: %w", err) + } + + return append(githubMCP.ListTools(), coderMCP.ListTools()...), nil } // nolint: revive diff --git a/coderd/coderd.go b/coderd/coderd.go index 77abf9fa6f48a..256743191b482 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -2011,60 +2011,6 @@ func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name strin return aibridgedproto.NewDRPCAIBridgeDaemonClient(clientSession), nil } -//// TODO: naming... -// func (api *API) CreateInMemoryOpenAIBridgeClient(dialCtx context.Context, srv *aibridged.Server) (client aibridgedproto.DRPCOpenAIServiceClient, err error) { -// // TODO(dannyk): implement options. -// // TODO(dannyk): implement tracing. -// -// clientSession, serverSession := drpcsdk.MemTransportPipe() -// defer func() { -// if err != nil { -// _ = clientSession.Close() -// _ = serverSession.Close() -// } -// }() -// -// // TODO(dannyk): implement API versioning. -// -// mux := drpcmux.New() -// api.Logger.Debug(dialCtx, "starting in-memory OpenAI bridge") -// logger := api.Logger.Named("inmem-openai-bridge") -// err = aibridgedproto.DRPCRegisterOpenAIService(mux, srv) -// if err != nil { -// return nil, err -// } -// server := drpcserver.NewWithOptions(&tracing.DRPCHandler{Handler: mux}, -// drpcserver.Options{ -// Manager: drpcsdk.DefaultDRPCOptions(nil), -// Log: func(err error) { -// if xerrors.Is(err, io.EOF) { -// return -// } -// logger.Debug(dialCtx, "drpc server error", slog.Error(err)) -// }, -// }, -// ) -// // in-mem pipes aren't technically "websockets" but they have the same properties as far as the -// // API is concerned: they are long-lived connections that we need to close before completing -// // shutdown of the API. -// api.WebsocketWaitMutex.Lock() -// api.WebsocketWaitGroup.Add(1) -// api.WebsocketWaitMutex.Unlock() -// go func() { -// defer api.WebsocketWaitGroup.Done() -// // Here we pass the background context, since we want the server to keep serving until the -// // client hangs up. The aibridged is local, in-mem, so there isn't a danger of losing contact with it and -// // having a dead connection we don't know the status of. -// err := server.Serve(context.Background(), serverSession) -// logger.Info(dialCtx, "OpenAI bridge disconnected", slog.Error(err)) -// // close the sessions, so we don't leak goroutines serving them. -// _ = clientSession.Close() -// _ = serverSession.Close() -// }() -// -// return aibridgedproto.NewDRPCOpenAIServiceClient(clientSession), nil -//} - func (api *API) DERPMap() *tailcfg.DERPMap { fn := api.DERPMapper.Load() if fn != nil { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index ace0170be3608..472e12a0d0683 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3229,7 +3229,7 @@ Write out the current server config as YAML to stdout.`, Flag: "ai-bridge-openai-base-url", Env: "CODER_AI_BRIDGE_OPENAI_BASE_URL", Value: &c.AI.BridgeConfig.OpenAI.BaseURL, - Default: "https://api.openai.com", + Default: "https://api.openai.com/v1/", Group: &deploymentGroupAIBridge, YAML: "openai_base_url", Hidden: true, @@ -3251,7 +3251,7 @@ Write out the current server config as YAML to stdout.`, Flag: "ai-bridge-anthropic-base-url", Env: "CODER_AI_BRIDGE_ANTHROPIC_BASE_URL", Value: &c.AI.BridgeConfig.Anthropic.BaseURL, - Default: "https://api.anthropic.com", + Default: "https://api.anthropic.com/", Group: &deploymentGroupAIBridge, YAML: "base_url", Hidden: true, @@ -3262,7 +3262,7 @@ Write out the current server config as YAML to stdout.`, Flag: "ai-bridge-anthropic-key", Env: "CODER_AI_BRIDGE_ANTHROPIC_KEY", Value: &c.AI.BridgeConfig.Anthropic.Key, - Default: "https://api.anthropic.com", + Default: "", Group: &deploymentGroupAIBridge, YAML: "key", Hidden: true, From 528358a3b5476e02cc84e870139f57d34287a42f Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 24 Jul 2025 17:02:28 +0200 Subject: [PATCH 41/61] improve error handling Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 4 --- aibridged/bridge.go | 65 ++++++------------------------------------ aibridged/constants.go | 5 ---- coderd/aibridge.go | 7 ----- 4 files changed, 8 insertions(+), 73 deletions(-) delete mode 100644 aibridged/constants.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 873b2a6da85ba..3fe471c8591b7 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -254,10 +254,6 @@ func (s *Server) BridgeAddr() string { return s.bridge.Addr() } -func (s *Server) BridgeErr() error { - return s.bridge.lastErr -} - // TODO: direct copy/paste from provisionerd, abstract into common util. func retryable(err error) bool { return xerrors.Is(err, yamux.ErrSessionShutdown) || xerrors.Is(err, io.EOF) || xerrors.Is(err, fasthttputil.ErrInmemoryListenerClosed) || diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 8162dcde6b4ba..f9dc236ff079c 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -132,9 +132,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { b.logger.Info(r.Context(), "openai request started", slog.F("session_id", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) - // Clear any previous error state - b.clearError() - defer func() { b.logger.Info(r.Context(), "openai request ended", slog.F("session_id", sessionID)) _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) @@ -321,15 +318,17 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } if err := stream.Err(); err != nil { + b.logger.Error(ctx, "server stream error", slog.Error(err)) var apierr *openai.Error if errors.As(err, &apierr) { - http.Error(w, apierr.Message, apierr.StatusCode) - return + events.TrySend(ctx, map[string]interface{}{ + "error": true, + "message": err.Error(), + }) + // http.Error(w, apierr.Message, apierr.StatusCode) + break } else if isConnectionError(err) { - b.logger.Debug(ctx, "upstream connection closed", slog.Error(err)) - } else { - b.logger.Error(ctx, "server stream error", slog.Error(err)) - b.setError(err) + b.logger.Warn(ctx, "upstream connection error", slog.Error(err)) } http.Error(w, err.Error(), http.StatusInternalServerError) @@ -433,7 +432,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } b.logger.Error(ctx, "chat completion failed", slog.Error(err)) - b.setError(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -582,9 +580,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { b.logger.Info(r.Context(), "anthropic request started", slog.F("session_id", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) - // Clear any previous error state - b.clearError() - defer func() { b.logger.Info(r.Context(), "anthropic request ended", slog.F("session_id", sessionID)) _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) @@ -633,7 +628,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // Policy examples. if strings.Contains(string(in.Model), "opus") { err := xerrors.Errorf("%q model is not allowed", in.Model) - b.setError(err) http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -1163,10 +1157,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if err != nil { b.logger.Error(ctx, "failed to send error", slog.Error(err)) } - - b.setError(antErr) - } else { - b.setError(streamErr) } } @@ -1258,45 +1248,6 @@ func (b *Bridge) Addr() string { return b.addr } -// setError sets a structured error with appropriate context -func (b *Bridge) setError(val any) { - switch err := val.(type) { - case error: - switch { - case errors.Is(err, context.Canceled): - b.lastErr = &BridgeError{ - Code: ErrorTypeRequestCanceled, - Message: "Request was canceled", - StatusCode: http.StatusRequestTimeout, - } - case isConnectionError(err): - b.lastErr = &BridgeError{ - Code: ErrorTypeConnectionError, - Message: "Connection to upstream service failed", - StatusCode: http.StatusBadGateway, - } - default: - b.lastErr = &BridgeError{ - Code: ErrorTypeUnexpectedError, - Message: err.Error(), - StatusCode: http.StatusInternalServerError, - } - } - case AnthropicErrorResponse: - b.lastErr = &BridgeError{ - Code: ErrorTypeAnthropicAPIError, - Message: err.Error.Message, - Details: map[string]string{"type": err.Error.Type}, - StatusCode: err.StatusCode, - } - } -} - -// clearError clears the error state when a new request starts -func (b *Bridge) clearError() { - b.lastErr = nil -} - // logConnectionError logs connection errors with appropriate severity func (b *Bridge) logConnectionError(ctx context.Context, err error, operation string) { if isConnectionError(err) { diff --git a/aibridged/constants.go b/aibridged/constants.go deleted file mode 100644 index c274f62ed120b..0000000000000 --- a/aibridged/constants.go +++ /dev/null @@ -1,5 +0,0 @@ -package aibridged - -const ( - ProxyErrCode = 1500 -) diff --git a/coderd/aibridge.go b/coderd/aibridge.go index 0ac72a0c8a66a..3184ae8691f06 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -27,13 +27,6 @@ func (r *rt) RoundTrip(req *http.Request) (*http.Response, error) { resp, err := r.RoundTripper.RoundTrip(req) - if err != nil || resp.StatusCode == aibridged.ProxyErrCode { - lastErr := r.server.BridgeErr() - if lastErr != nil { - return resp, lastErr - } - } - return resp, err } From 1760a91c43ed1493410953fedc9047c6e243da2d Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 25 Jul 2025 15:49:19 +0200 Subject: [PATCH 42/61] bridge-per-key model, autoinject coder mcp using local mcp server Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 91 +++-------------------------- aibridged/bridge.go | 14 ++--- aibridged/middleware.go | 6 +- cli/server.go | 61 +------------------ coderd/aibridge.go | 104 ++++++++++++++++++++++++++++----- coderd/coderd.go | 5 ++ coderd/httpmw/cors.go | 2 +- go.mod | 4 +- site/src/api/typesGenerated.ts | 8 ++- 9 files changed, 124 insertions(+), 171 deletions(-) diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 3fe471c8591b7..4b9893bb88652 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -48,13 +48,11 @@ type Server struct { shuttingDownB bool // shuttingDownCh will receive when we start graceful shutdown shuttingDownCh chan struct{} - - bridge *Bridge } var _ proto.DRPCAIBridgeDaemonServer = &Server{} -func New(rpcDialer Dialer, httpAddr string, logger slog.Logger, bridgeCfg codersdk.AIBridgeConfig, tools []*MCPTool) (*Server, error) { +func New(rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) { if rpcDialer == nil { return nil, xerrors.Errorf("nil rpcDialer given") } @@ -71,29 +69,13 @@ func New(rpcDialer Dialer, httpAddr string, logger slog.Logger, bridgeCfg coders initConnectionCh: make(chan struct{}), } - // TODO: improve error handling here; if this fails it prevents the whole server from starting up! - - bridge, err := NewBridge(bridgeCfg, httpAddr, logger.Named("ai_bridge"), daemon.client, tools) - if err != nil { - return nil, xerrors.Errorf("create new bridge server: %w", err) - } - - daemon.bridge = bridge - daemon.wg.Add(1) go daemon.connect() - daemon.wg.Add(1) - go func() { - defer daemon.wg.Done() - err := bridge.Serve() - // TODO: better error handling. - // TODO: close on shutdown. - logger.Error(ctx, "bridge server stopped", slog.Error(err)) - }() - return daemon, nil -} // Connect establishes a connection to coderd. +} + +// Connect establishes a connection to coderd. func (s *Server) connect() { defer s.logger.Debug(s.closeContext, "connect loop exited") defer s.wg.Done() @@ -155,7 +137,7 @@ connectLoop: } } -func (s *Server) client() (proto.DRPCAIBridgeDaemonClient, bool) { +func (s *Server) Client() (proto.DRPCAIBridgeDaemonClient, bool) { select { case <-s.closeContext.Done(): return nil, false @@ -168,7 +150,7 @@ func (s *Server) client() (proto.DRPCAIBridgeDaemonClient, bool) { } func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { - out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackTokenUsageResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackTokenUsageResponse, error) { return client.TrackTokenUsage(ctx, in) }) if err != nil { @@ -178,7 +160,7 @@ func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageR } func (s *Server) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptRequest) (*proto.TrackUserPromptResponse, error) { - out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackUserPromptResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackUserPromptResponse, error) { return client.TrackUserPrompt(ctx, in) }) if err != nil { @@ -188,7 +170,7 @@ func (s *Server) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptR } func (s *Server) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageRequest) (*proto.TrackToolUsageResponse, error) { - out, err := clientDoWithRetries(ctx, s.client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackToolUsageResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackToolUsageResponse, error) { return client.TrackToolUsage(ctx, in) }) if err != nil { @@ -197,63 +179,6 @@ func (s *Server) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageReq return out, nil } -// func (s *Server) ChatCompletions(payload *proto.JSONPayload, stream proto.DRPCOpenAIService_ChatCompletionsStream) error { -// // TODO: call OpenAI API. -// -// select { -// case <-stream.Context().Done(): -// return nil -// default: -// } -// -// err := stream.Send(&proto.JSONPayload{ -// Content: ` -//{ -// "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT", -// "object": "chat.completion", -// "created": 1741569952, -// "model": "gpt-4.1-2025-04-14", -// "choices": [ -// { -// "index": 0, -// "message": { -// "role": "assistant", -// "content": "Hello! How can I assist you today?", -// "refusal": null, -// "annotations": [] -// }, -// "logprobs": null, -// "finish_reason": "stop" -// } -// ], -// "usage": { -// "prompt_tokens": 19, -// "completion_tokens": 10, -// "total_tokens": 29, -// "prompt_tokens_details": { -// "cached_tokens": 0, -// "audio_tokens": 0 -// }, -// "completion_tokens_details": { -// "reasoning_tokens": 0, -// "audio_tokens": 0, -// "accepted_prediction_tokens": 0, -// "rejected_prediction_tokens": 0 -// } -// }, -// "service_tier": "default" -//} -// `}) -// if err != nil { -// return xerrors.Errorf("stream chat completion response: %w", err) -// } -// return nil -//} - -func (s *Server) BridgeAddr() string { - return s.bridge.Addr() -} - // TODO: direct copy/paste from provisionerd, abstract into common util. func retryable(err error) bool { return xerrors.Is(err, yamux.ErrSessionShutdown) || xerrors.Is(err, io.EOF) || xerrors.Is(err, fasthttputil.ErrInmemoryListenerClosed) || diff --git a/aibridged/bridge.go b/aibridged/bridge.go index f9dc236ff079c..158e9797686b9 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -76,14 +76,13 @@ type Bridge struct { cfg codersdk.AIBridgeConfig httpSrv *http.Server - addr string clientFn func() (proto.DRPCAIBridgeDaemonClient, bool) logger slog.Logger tools map[string]*MCPTool } -func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool), tools []*MCPTool) (*Bridge, error) { +func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool), tools []*MCPTool) (*Bridge, error) { var bridge Bridge mux := &http.ServeMux{} @@ -91,7 +90,6 @@ func NewBridge(cfg codersdk.AIBridgeConfig, addr string, logger slog.Logger, cli mux.HandleFunc("/v1/messages", bridge.proxyAnthropicRequest) srv := &http.Server{ - Addr: addr, Handler: mux, // TODO: configurable. @@ -123,6 +121,10 @@ func (b *Bridge) openAITarget() *url.URL { return target } +func (b *Bridge) Handler() http.Handler { + return b.httpSrv.Handler +} + // proxyOpenAIRequest intercepts, filters, augments, and delivers requests & responses from client to upstream and back. // // References: @@ -1239,15 +1241,9 @@ func (b *Bridge) Serve() error { return xerrors.Errorf("listen: %w", err) } - b.addr = list.Addr().String() - return b.httpSrv.Serve(list) // TODO: TLS. } -func (b *Bridge) Addr() string { - return b.addr -} - // logConnectionError logs connection errors with appropriate severity func (b *Bridge) logConnectionError(ctx context.Context, err error, operation string) { if isConnectionError(err) { diff --git a/aibridged/middleware.go b/aibridged/middleware.go index 02896777ad1d9..23bb9d6a2f1ef 100644 --- a/aibridged/middleware.go +++ b/aibridged/middleware.go @@ -2,6 +2,7 @@ package aibridged import ( "bytes" + "context" "crypto/subtle" "net/http" @@ -9,6 +10,8 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" ) +type ContextKeyBridgeAPIKey struct{} + // AuthMiddleware extracts and validates authorization tokens for AI bridge endpoints. // It supports both Bearer tokens in Authorization headers and Coder session tokens // from cookies/headers following the same patterns as existing Coder authentication. @@ -34,7 +37,8 @@ func AuthMiddleware(db database.Store) func(http.Handler) http.Handler { return } - next.ServeHTTP(rw, r) + // Pass request with modify context including the request token. + next.ServeHTTP(rw, r.WithContext(context.WithValue(ctx, ContextKeyBridgeAPIKey{}, token))) }) } } diff --git a/cli/server.go b/cli/server.go index 2665d5792082d..3a2eb4b1d7306 100644 --- a/cli/server.go +++ b/cli/server.go @@ -31,6 +31,7 @@ import ( "sync/atomic" "time" + "github.com/ammario/tlru" "github.com/charmbracelet/lipgloss" "github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-systemd/daemon" @@ -1125,6 +1126,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. aiBridgeDaemons = append(aiBridgeDaemons, daemon) } coderAPI.AIBridgeDaemons = aiBridgeDaemons + coderAPI.AIBridges = tlru.New[string](tlru.ConstantCost[*aibridged.Bridge], 100) // TODO: configurable. // Updates the systemd status from activating to activated. _, err = daemon.SdNotify(false, daemon.SdNotifyReady) @@ -1571,68 +1573,11 @@ func newProvisionerDaemon( func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string, bridgeCfg codersdk.AIBridgeConfig) (*aibridged.Server, error) { httpAddr := "0.0.0.0:0" // TODO: configurable. - - // TODO: in reality, it won't work this way. We'll have to load the tools dynamically - tools, err := loadMCP(coderAPI.Logger.Named("mcp-tools")) - if err != nil { - coderAPI.Logger.Error(ctx, "failed to load MCP tools", slog.Error(err)) - } - return aibridged.New(func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { // This debounces calls to listen every second. // TODO: is this true / necessary? return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx, name) - }, httpAddr, coderAPI.Logger.Named("aibridged").With(slog.F("name", name)), bridgeCfg, tools) -} - -func loadMCP(logger slog.Logger) ([]*aibridged.MCPTool, error) { - const ( - githubMCPName = "github" - coderMCPName = "coder" - ) - githubMCP, err := aibridged.NewMCPToolBridge(githubMCPName, "https://api.githubcopilot.com/mcp/", map[string]string{ - "Authorization": "Bearer " + os.Getenv("GITHUB_MCP_TOKEN"), - }, logger.Named("mcp-bridge-github")) - if err != nil { - return nil, xerrors.Errorf("github MCP bridge setup: %w", err) - } - coderMCP, err := aibridged.NewMCPToolBridge(coderMCPName, "https://dev.coder.com/api/experimental/mcp/http", map[string]string{ - // "Authorization": "Bearer " + os.Getenv("CODER_MCP_TOKEN"), - // This is necessary to even access the MCP endpoint. - "Coder-Session-Token": os.Getenv("CODER_MCP_SESSION_TOKEN"), - }, logger.Named("mcp-bridge-coder")) - if err != nil { - return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) - } - - var eg errgroup.Group - eg.Go(func() error { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - err := githubMCP.Init(ctx) - if err == nil { - return nil - } - return xerrors.Errorf("github: %w", err) - }) - eg.Go(func() error { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - err := coderMCP.Init(ctx) - if err == nil { - return nil - } - return xerrors.Errorf("coder: %w", err) - }) - - // This must block requests until MCP proxies are setup. - if err := eg.Wait(); err != nil { - return nil, xerrors.Errorf("MCP proxy init: %w", err) - } - - return append(githubMCP.ListTools(), coderMCP.ListTools()...), nil + }, httpAddr, coderAPI.Logger.Named("aibridged").With(slog.F("name", name))) } // nolint: revive diff --git a/coderd/aibridge.go b/coderd/aibridge.go index 3184ae8691f06..a1791dd25b0af 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -1,15 +1,18 @@ package coderd import ( + "context" "fmt" "net/http" - "net/http/httputil" - "net/url" "time" + "golang.org/x/sync/errgroup" + "golang.org/x/xerrors" + "cdr.dev/slog" "github.com/coder/coder/v2/aibridged" + aibridgedproto "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/coderd/util/slice" ) @@ -33,10 +36,6 @@ func (r *rt) RoundTrip(req *http.Request) (*http.Response, error) { func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - // Something, somewhere is adding a duplicate header. - // Haven't been able to track it down yet. - // rw.Header().Del("Access-Control-Allow-Origin") - if len(api.AIBridgeDaemons) == 0 { http.Error(rw, "no AI bridge daemons running", http.StatusInternalServerError) return @@ -51,19 +50,92 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { return } - u, err := url.Parse(fmt.Sprintf("http://%s", server.BridgeAddr())) // TODO: TLS. - if err != nil { - api.Logger.Error(ctx, "failed to parse bridge address", slog.Error(err)) - http.Error(rw, "failed to parse bridge address", http.StatusInternalServerError) + key, ok := r.Context().Value(aibridged.ContextKeyBridgeAPIKey{}).(string) + if key == "" || !ok { + http.Error(rw, "unable to retrieve request session key", http.StatusBadRequest) return } - rp := httputil.NewSingleHostReverseProxy(u) - rp.Transport = &rt{RoundTripper: http.DefaultTransport, server: server} - rp.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - api.Logger.Error(ctx, "aibridge reverse proxy error", slog.Error(err)) - http.Error(rw, err.Error(), http.StatusInternalServerError) + bridge, err := api.createOrLoadBridgeForAPIKey(ctx, key, server.Client) + if err != nil { + api.Logger.Error(ctx, "failed to create ai bridge", slog.Error(err)) + http.Error(rw, "failed to create ai bridge", http.StatusInternalServerError) return } - http.StripPrefix("/api/v2/aibridge", rp).ServeHTTP(rw, r) + + // TODO: direct func calls instead of HTTP srv? + done := make(chan any, 1) + go func() { + defer close(done) + err := bridge.Serve() + // TODO: better error handling. + // TODO: close on shutdown. + api.Logger.Warn(ctx, "bridge server stopped", slog.Error(err)) + }() + + http.StripPrefix("/api/v2/aibridge", bridge.Handler()).ServeHTTP(rw, r) +} + +func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, clientFn func() (aibridgedproto.DRPCAIBridgeDaemonClient, bool)) (*aibridged.Bridge, error) { + if api.AIBridges == nil { + return nil, xerrors.New("bridge cache storage is not configured") + } + + api.AIBridgesMu.RLock() + val, _, ok := api.AIBridges.Get(key) + api.AIBridgesMu.RUnlock() + + // TODO: TOCTOU potential here + // TODO: track startup time since it adds latency to first request (histogram count will also help us see how often this occurs) + if !ok { + api.AIBridgesMu.Lock() + defer api.AIBridgesMu.Unlock() + + tools, err := api.fetchTools(ctx, api.Logger, key) + if err != nil { + api.Logger.Warn(ctx, "failed to load tools", slog.Error(err)) + } + + bridge, err := aibridged.NewBridge(api.DeploymentValues.AI.BridgeConfig, api.Logger.Named("ai_bridge"), clientFn, tools) + if err != nil { + return nil, xerrors.Errorf("create new bridge server: %w", err) + } + + api.Logger.Info(ctx, "created bridge") // TODO: improve usefulness; log user ID? + + api.AIBridges.Set(key, bridge, time.Minute) // TODO: configurable. + val = bridge + } + + return val, nil +} + +func (api *API) fetchTools(ctx context.Context, logger slog.Logger, key string) ([]*aibridged.MCPTool, error) { + url := api.DeploymentValues.AccessURL.String() + "/api/experimental/mcp/http" + coderMCP, err := aibridged.NewMCPToolBridge("coder", url, map[string]string{ + "Coder-Session-Token": key, + }, logger.Named("mcp-bridge-coder")) + if err != nil { + return nil, xerrors.Errorf("coder MCP bridge setup: %w", err) + } + + // TODO: add github mcp if external auth is configured. + var eg errgroup.Group + eg.Go(func() error { + ctx, cancel := context.WithTimeout(ctx, time.Second*30) + defer cancel() + + err := coderMCP.Init(ctx) + if err == nil { + return nil + } + return xerrors.Errorf("coder: %w", err) + }) + + // This must block requests until MCP proxies are setup. + if err := eg.Wait(); err != nil { + return nil, xerrors.Errorf("MCP proxy init: %w", err) + } + + return coderMCP.ListTools(), nil } diff --git a/coderd/coderd.go b/coderd/coderd.go index 256743191b482..af2f6f01f37b4 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -19,6 +19,8 @@ import ( "sync/atomic" "time" + "github.com/ammario/tlru" + "github.com/coder/coder/v2/coderd/oauth2provider" "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/wsbuilder" @@ -1551,6 +1553,7 @@ func New(options *Options) *API { r.Use(aibridged.AuthMiddleware(api.Database)) r.Post("/v1/chat/completions", api.bridgeAIRequest) r.Get("/v1/models", func(rw http.ResponseWriter, r *http.Request) { + // TODO: reverse-proxy blindly to upstream, or implement using policies to control available models. httpapi.Write(context.Background(), rw, http.StatusOK, map[string]any{ "object": "list", "data": []map[string]any{ @@ -1724,6 +1727,8 @@ type API struct { dbRolluper *dbrollup.Rolluper AIBridgeDaemons []*aibridged.Server + AIBridges *tlru.Cache[string, *aibridged.Bridge] + AIBridgesMu sync.RWMutex } // Close waits for all WebSocket connections to drain before returning. diff --git a/coderd/httpmw/cors.go b/coderd/httpmw/cors.go index 128605788704a..13250112c1ede 100644 --- a/coderd/httpmw/cors.go +++ b/coderd/httpmw/cors.go @@ -127,7 +127,7 @@ func PermissiveCors() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH, SNARF") // TODO: remove SNARF. + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH") w.Header().Set("Access-Control-Allow-Headers", "*") w.Header().Set("Access-Control-Max-Age", "86400") diff --git a/go.mod b/go.mod index c07547c6c1a9e..1e0e047a114fb 100644 --- a/go.mod +++ b/go.mod @@ -349,7 +349,7 @@ require ( github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect - github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 + github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect github.com/jsimonetti/rtnetlink v1.3.5 // indirect @@ -487,6 +487,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 github.com/mark3labs/mcp-go v0.34.0 github.com/openai/openai-go v1.8.1 + github.com/tidwall/sjson v1.2.5 ) require ( @@ -526,7 +527,6 @@ require ( github.com/sergeymakinen/go-bmp v1.0.0 // indirect github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect - github.com/tidwall/sjson v1.2.5 // indirect github.com/tmaxmax/go-sse v0.11.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0110da89efd78..e6490f63d94ff 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -15,10 +15,16 @@ export interface AIBridgeAnthropicConfig { // From codersdk/deployment.go export interface AIBridgeConfig { readonly daemons: number; - readonly openai_base_url: string; + readonly openai: AIBridgeOpenAIConfig; readonly anthropic: AIBridgeAnthropicConfig; } +// From codersdk/deployment.go +export interface AIBridgeOpenAIConfig { + readonly base_url: string; + readonly key: string; +} + // From codersdk/deployment.go export interface AIConfig { readonly bridge?: AIBridgeConfig; From bd6cc944af8dc5a8c0a18491bc4ea6fa7adf6fed Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 28 Jul 2025 12:10:34 +0200 Subject: [PATCH 43/61] fixup tool calling Signed-off-by: Danny Kopping --- aibridged/bridge.go | 29 +++++------------------------ aibridged/mcp.go | 6 +++++- coderd/aibridge.go | 11 ----------- 3 files changed, 10 insertions(+), 36 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 158e9797686b9..a9af7c8eb3af7 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -494,7 +494,8 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { appendedPrevMsg := false // Process each pending tool call for _, tc := range pendingToolCalls { - if !b.isInjectedTool(tc.ID) { + fn := tc.Function.Name + if !b.isInjectedTool(fn) { // Not an MCP proxy call, don't do anything. continue } @@ -508,7 +509,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ Model: string(in.Model), Input: tc.Function.Arguments, - Tool: tc.Function.Name, + Tool: fn, Injected: true, }) if err != nil { @@ -521,7 +522,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { ) _ = json.NewEncoder(&buf).Encode(tc.Function.Arguments) _ = json.NewDecoder(&buf).Decode(&args) - res, err := b.tools[tc.ID].Call(ctx, args) + res, err := b.tools[fn].Call(ctx, args) if err != nil { // Always provide a tool result even if the tool call failed errorResponse := map[string]interface{}{ @@ -759,6 +760,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { break } + // TODO: implement injected tool calling. if resp.StopReason == anthropic.BetaStopReasonToolUse { var ( toolUse anthropic.BetaToolUseBlock @@ -809,12 +811,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { es := newEventStream(anthropicEventStream) - // var buf strings.Builder - // in.Messages[0].Content = []anthropic.BetaContentBlockParamUnion{in.Messages[0].Content[len(in.Messages[0].Content) - 1]} - // - // json.NewEncoder(&buf).Encode(in) - // fmt.Println(strings.Replace(buf.String(), "'", "\\'", -1)) - var wg sync.WaitGroup wg.Add(1) @@ -862,8 +858,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // Don't relay this event back, otherwise the client will try invoke the tool as well. continue } - default: - fmt.Printf("[%s] %s\n", event.Type, event.RawJSON()) } case string(constant.ValueOf[ant_constant.ContentBlockDelta]()): if len(pendingToolCalls) > 0 && b.isInjectedTool(lastToolName) { @@ -1179,19 +1173,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } <-streamCtx.Done() - - // TODO: do we need to do this? - // // Close the underlying connection by hijacking it - // if hijacker, ok := w.(http.Hijacker); ok { - // conn, _, err := hijacker.Hijack() - // if err != nil { - // b.logger.Error(ctx, "failed to hijack connection", slog.Error(err)) - // } else { - // conn.Close() // This closes the TCP connection entirely - // b.logger.Debug(ctx, "connection closed, stream over") - // } - // } - break } } diff --git a/aibridged/mcp.go b/aibridged/mcp.go index 966b8aeb21d96..de1bd356d5e3f 100644 --- a/aibridged/mcp.go +++ b/aibridged/mcp.go @@ -96,6 +96,10 @@ func (b *MCPToolBridge) CallTool(ctx context.Context, name string, input any) (* } func (t *MCPTool) Call(ctx context.Context, input any) (*mcp.CallToolResult, error) { + if t == nil { + return nil, xerrors.Errorf("nil tool!") + } + return t.client.CallTool(ctx, mcp.CallToolRequest{ Params: mcp.CallToolParams{ Name: t.Name, @@ -119,7 +123,7 @@ func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]*MCPTool, if err != nil { return nil, xerrors.Errorf("init MCP client: %w", err) } - fmt.Printf("mcp(%q)], %+v\n", result.ServerInfo.Name, result) // TODO: remove. + b.logger.Debug(ctx, "mcp client initialized", slog.F("name", result.ServerInfo.Name), slog.F("server_version", result.ServerInfo.Version)) // Test tool listing tools, err := b.client.ListTools(ctx, mcp.ListToolsRequest{}) diff --git a/coderd/aibridge.go b/coderd/aibridge.go index a1791dd25b0af..e550074ea0335 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -62,17 +62,6 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { http.Error(rw, "failed to create ai bridge", http.StatusInternalServerError) return } - - // TODO: direct func calls instead of HTTP srv? - done := make(chan any, 1) - go func() { - defer close(done) - err := bridge.Serve() - // TODO: better error handling. - // TODO: close on shutdown. - api.Logger.Warn(ctx, "bridge server stopped", slog.Error(err)) - }() - http.StripPrefix("/api/v2/aibridge", bridge.Handler()).ServeHTTP(rw, r) } From 8e88565f921107247faeb1801290b87e2120d057 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 29 Jul 2025 08:40:13 +0200 Subject: [PATCH 44/61] non-streaming injected tool calls for anthropic Signed-off-by: Danny Kopping --- aibridged/bridge.go | 188 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 150 insertions(+), 38 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index a9af7c8eb3af7..8ffaca2b940d6 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -722,9 +722,8 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") client := anthropic.NewClient(opts...) if !in.UseStreaming() { - var resp *anthropic.BetaMessage for { - resp, err = client.Beta.Messages.New(ctx, messages, opts...) + resp, err := client.Beta.Messages.New(ctx, messages, opts...) if err != nil { if isConnectionError(err) { b.logger.Warn(ctx, "upstream connection closed", slog.Error(err)) @@ -754,56 +753,169 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) } - messages.Messages = append(messages.Messages, resp.ToParam()) + // Handle tool calls for non-streaming. + var pendingToolCalls []anthropic.BetaToolUseBlock + for _, c := range resp.Content { + toolUse := c.AsToolUse() + if toolUse.ID == "" { + continue + } + + if b.isInjectedTool(toolUse.Name) { + pendingToolCalls = append(pendingToolCalls, toolUse) + continue + } - if resp.StopReason == anthropic.BetaStopReasonEndTurn { + // If tool is not injected, track it since the client will be handling it. + if serialized, err := json.Marshal(toolUse.Input); err == nil { + _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + Model: string(resp.Model), + Input: string(serialized), + Tool: toolUse.Name, + }) + if err != nil { + b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) + } + } else { + b.logger.Warn(ctx, "failed to marshal args for tool usage", slog.Error(err)) + } + } + + // If no injected tool calls, we're done. + if len(pendingToolCalls) == 0 { + out, err := json.Marshal(resp) + if err != nil { + http.Error(w, "error marshaling response", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(out) break } - // TODO: implement injected tool calling. - if resp.StopReason == anthropic.BetaStopReasonToolUse { - var ( - toolUse anthropic.BetaToolUseBlock - input any - ) - for _, c := range resp.Content { - toolUse = c.AsToolUse() - if toolUse.ID == "" { - continue - } + // Append the assistant's message (which contains the tool_use block) + // to the messages for the next API call. + messages.Messages = append(messages.Messages, resp.ToParam()) - input = toolUse.Input + // Process each pending tool call. + for _, tc := range pendingToolCalls { + tool := b.tools[tc.Name] + + var args map[string]any + serialized, err := json.Marshal(tc.Input) + if err != nil { + b.logger.Warn(ctx, "failed to marshal tool args for unmarshal", slog.Error(err), slog.F("tool", tc.Name)) + // Continue to next tool call, but still append an error tool_result + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error unmarshaling tool arguments: %v", err), true)), + ) + continue + } else if err := json.Unmarshal(serialized, &args); err != nil { + b.logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", tc.Name)) + // Continue to next tool call, but still append an error tool_result + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error unmarshaling tool arguments: %v", err), true)), + ) + continue } - if input != nil { - if serialized, err := json.Marshal(input); err == nil { - _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - Model: string(resp.Model), - Input: string(serialized), - Tool: toolUse.Name, - Injected: b.isInjectedTool(toolUse.Name), + _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + Model: string(resp.Model), + Input: string(serialized), + Tool: tc.Name, + Injected: true, + }) + if err != nil { + b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + } + + res, err := tool.Call(ctx, args) + if err != nil { + // Always provide a tool_result even if the tool call failed + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error calling tool: %v", err), true)), + ) + continue + } + + // Ensure at least one tool_result is always added for each tool_use. + toolResult := anthropic.BetaContentBlockParamUnion{ + OfToolResult: &anthropic.BetaToolResultBlockParam{ + ToolUseID: tc.ID, + IsError: anthropic.Bool(false), + }, + } + + var hasValidResult bool + for _, content := range res.Content { + switch cb := content.(type) { + case mcp.TextContent: + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: cb.Text, + }, }) - if err != nil { - b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) + hasValidResult = true + case mcp.EmbeddedResource: + switch resource := cb.Resource.(type) { + case mcp.TextResourceContents: + val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", + resource.MIMEType, resource.URI, resource.Text) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: val, + }, + }) + hasValidResult = true + case mcp.BlobResourceContents: + val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", + resource.MIMEType, resource.URI, resource.Blob) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: val, + }, + }) + hasValidResult = true + default: + b.logger.Error(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource))) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: unknown embedded resource type", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + hasValidResult = true } - } else { - b.logger.Warn(ctx, "failed to marshal args for tool usage", slog.Error(err)) + default: + b.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb))) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: unsupported tool result type", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + hasValidResult = true } } - break - } - } + // If no content was processed, still add a tool_result + if !hasValidResult { + b.logger.Error(ctx, "no tool result added", slog.F("content_len", len(res.Content)), slog.F("is_error", res.IsError)) // This can only happen if there's somehow no content. + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: no valid tool result content", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + } - out, err := json.Marshal(resp) - if err != nil { - http.Error(w, "error marshaling response", http.StatusInternalServerError) - return + if len(toolResult.OfToolResult.Content) > 0 { + messages.Messages = append(messages.Messages, anthropic.NewBetaUserMessage(toolResult)) + } + } } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(out) return } From ed91ba860e9d9df2039dc5b818e5b02a7d75ee94 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 29 Jul 2025 08:48:57 +0200 Subject: [PATCH 45/61] fix tests Signed-off-by: Danny Kopping --- aibridged/bridge.go | 10 ---------- aibridged/bridge_test.go | 35 ++++++++++------------------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 8ffaca2b940d6..92125fa1b349d 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "net" "net/http" "net/http/httputil" "net/url" @@ -1328,15 +1327,6 @@ type AnthropicErrorResponse struct { StatusCode int `json:"-"` } -func (b *Bridge) Serve() error { - list, err := net.Listen("tcp", b.httpSrv.Addr) - if err != nil { - return xerrors.Errorf("listen: %w", err) - } - - return b.httpSrv.Serve(list) // TODO: TLS. -} - // logConnectionError logs connection errors with appropriate severity func (b *Bridge) logConnectionError(ctx context.Context, err error, operation string) { if isConnectionError(err) { diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index a5b06478d7e3b..551f0d86c6de3 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -107,19 +107,14 @@ func TestAnthropicMessages(t *testing.T) { BaseURL: serpent.String(srv.URL), Key: serpent.String(sessionToken), }, - }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { return coderdClient, true }, nil) require.NoError(t, err) - go func() { - assert.NoError(t, b.Serve()) - }() - // Wait for bridge to come up. - require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) - + mockSrv := httptest.NewServer(b.Handler()) // Make API call to aibridge for Anthropic /v1/messages - req := createAnthropicMessagesReq(t, "http://"+b.Addr(), reqBody) + req := createAnthropicMessagesReq(t, mockSrv.URL, reqBody) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) @@ -214,19 +209,14 @@ func TestOpenAIChatCompletions(t *testing.T) { BaseURL: serpent.String(srv.URL), Key: serpent.String(sessionToken), }, - }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { return coderdClient, true }, nil) require.NoError(t, err) - go func() { - assert.NoError(t, b.Serve()) - }() - // Wait for bridge to come up. - require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) - + mockSrv := httptest.NewServer(b.Handler()) // Make API call to aibridge for OpenAI /v1/chat/completions - req := createOpenAIChatCompletionsReq(t, "http://"+b.Addr(), reqBody) + req := createOpenAIChatCompletionsReq(t, mockSrv.URL, reqBody) client := &http.Client{} resp, err := client.Do(req) @@ -289,7 +279,7 @@ func TestSimple(t *testing.T) { BaseURL: serpent.String(addr), Key: serpent.String(sessionToken), }, - }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { return client, true }, nil) }, @@ -306,7 +296,7 @@ func TestSimple(t *testing.T) { BaseURL: serpent.String(addr), Key: serpent.String(sessionToken), }, - }, "127.0.0.1:0", logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { return client, true }, nil) }, @@ -355,14 +345,9 @@ func TestSimple(t *testing.T) { b, err := tc.configureFunc(srv.URL, coderdClient) require.NoError(t, err) - go func() { - assert.NoError(t, b.Serve()) - }() - // Wait for bridge to come up. - require.Eventually(t, func() bool { return len(b.Addr()) > 0 }, testutil.WaitLong, testutil.IntervalFast) - + mockSrv := httptest.NewServer(b.Handler()) // When: calling the "API server" with the fixture's request body. - req := tc.createRequest(t, "http://"+b.Addr(), reqBody) + req := tc.createRequest(t, mockSrv.URL, reqBody) client := &http.Client{} resp, err := client.Do(req) require.NoError(t, err) From bd90c83f8773670c5c8f77e7eacd03420531ee58 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 4 Aug 2025 14:50:18 +0200 Subject: [PATCH 46/61] Test tool calls to Anthropic Signed-off-by: Danny Kopping --- aibridged/bridge.go | 2 +- aibridged/bridge_test.go | 210 ++++++++++++++++-- .../anthropic/single_injected_tool.txtar | 125 +++++++++++ 3 files changed, 318 insertions(+), 19 deletions(-) create mode 100644 aibridged/fixtures/anthropic/single_injected_tool.txtar diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 92125fa1b349d..2bd9fadea62b8 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -954,7 +954,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if err := message.Accumulate(event); err != nil { b.logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) http.Error(w, "failed to proxy request", http.StatusInternalServerError) - return + return // TODO: don't return, skip to close. } // Tool-related handling. diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index 551f0d86c6de3..efa5e68b2bd18 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -12,15 +12,21 @@ import ( "net/http" "net/http/httptest" "sync" + "sync/atomic" "testing" "golang.org/x/tools/txtar" "storj.io/drpc" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/packages/ssestream" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/sjson" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/coder/coder/v2/aibridged" "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/coderd/coderdtest" @@ -33,6 +39,8 @@ import ( var ( //go:embed fixtures/anthropic/single_builtin_tool.txtar antSingleBuiltinTool []byte + //go:embed fixtures/anthropic/single_injected_tool.txtar + antSingleInjectedTool []byte //go:embed fixtures/openai/single_builtin_tool.txtar oaiSingleBuiltinTool []byte @@ -45,15 +53,18 @@ var ( ) const ( - fixtureRequest = "request" - fixtureStreamingResponse = "streaming" - fixtureNonStreamingResponse = "non-streaming" + fixtureRequest = "request" + fixtureStreamingResponse = "streaming" + fixtureNonStreamingResponse = "non-streaming" + fixtureStreamingToolResponse = "streaming/tool-call" + fixtureNonStreamingToolResponse = "non-streaming/tool-call" ) func TestAnthropicMessages(t *testing.T) { t.Parallel() - sessionToken := getSessionToken(t) + client := coderdtest.New(t, nil) + sessionToken := getSessionToken(t, client) t.Run("single builtin tool", func(t *testing.T) { t.Parallel() @@ -95,7 +106,7 @@ func TestAnthropicMessages(t *testing.T) { reqBody = newBody ctx := testutil.Context(t, testutil.WaitLong) - srv := newMockServer(ctx, t, files) + srv := newMockServer(ctx, t, files, nil) t.Cleanup(srv.Close) coderdClient := &fakeBridgeDaemonClient{} @@ -150,12 +161,138 @@ func TestAnthropicMessages(t *testing.T) { }) } }) + + t.Run("single injected tool", func(t *testing.T) { + t.Parallel() + + cases := []struct { + streaming bool + }{ + { + streaming: true, + }, + { + streaming: false, + }, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("%s/streaming=%v", t.Name(), tc.streaming), func(t *testing.T) { + t.Parallel() + + arc := txtar.Parse(antSingleInjectedTool) + t.Logf("%s: %s", t.Name(), arc.Comment) + + files := filesMap(arc) + require.Len(t, files, 5) + require.Contains(t, files, fixtureRequest) + require.Contains(t, files, fixtureStreamingResponse) + require.Contains(t, files, fixtureNonStreamingResponse) + require.Contains(t, files, fixtureStreamingToolResponse) + require.Contains(t, files, fixtureNonStreamingToolResponse) + + reqBody := files[fixtureRequest] + + // Add the stream param to the request. + newBody, err := sjson.SetBytes(reqBody, "stream", tc.streaming) + require.NoError(t, err) + reqBody = newBody + + ctx := testutil.Context(t, testutil.WaitLong) + // Conditionally return fixtures based on request count. + // First request: halts with tool call instruction. + // Second request: tool call invocation. + mockSrv := newMockServer(ctx, t, files, func(reqCount uint32, resp []byte) []byte { + if reqCount == 1 { + return resp + } + + if reqCount > 2 { + t.Fatalf("did not expect more than 2 calls; received %d", reqCount) + } + + if !tc.streaming { + return files[fixtureNonStreamingToolResponse] + } + return files[fixtureStreamingToolResponse] + }) + t.Cleanup(mockSrv.Close) + + coderdClient := &fakeBridgeDaemonClient{} + logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + + // Setup Coder MCP integration. + mcpSrv := httptest.NewServer(createMockMCPSrv(t)) + mcpBridge, err := aibridged.NewMCPToolBridge("coder", mcpSrv.URL, map[string]string{}, logger) + require.NoError(t, err) + + // Initialize MCP client, fetch tools, and inject into bridge. + require.NoError(t, mcpBridge.Init(testutil.Context(t, testutil.WaitShort))) + tools := mcpBridge.ListTools() + require.NotEmpty(t, tools) + + b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + Anthropic: codersdk.AIBridgeAnthropicConfig{ + BaseURL: serpent.String(mockSrv.URL), + Key: serpent.String(sessionToken), + }, + }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return coderdClient, true + }, tools) + require.NoError(t, err) + + // Invoke request to mocked API via aibridge. + bridgeSrv := httptest.NewServer(b.Handler()) + req := createAnthropicMessagesReq(t, bridgeSrv.URL, reqBody) + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() + + // We must ALWAYS have 2 calls to the bridge. + require.Eventually(t, func() bool { return mockSrv.callCount.Load() == 2 }, testutil.WaitLong, testutil.IntervalFast) + + // TODO: this is a bit flimsy since this API won't be in beta forever. + var content *anthropic.BetaContentBlockUnion + if tc.streaming { + // Parse the response stream. + decoder := ssestream.NewDecoder(resp) + stream := ssestream.NewStream[anthropic.BetaRawMessageStreamEventUnion](decoder, nil) + var message anthropic.BetaMessage + for stream.Next() { + event := stream.Current() + require.NoError(t, message.Accumulate(event)) + } + require.NoError(t, stream.Err()) + require.Len(t, message.Content, 2) + content = &message.Content[1] + } else { + // Parse & unmarshal the response. + out, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + // TODO: this is a bit flimsy since this API won't be in beta forever. + var message anthropic.BetaMessage + require.NoError(t, json.Unmarshal(out, &message)) + require.NotNil(t, message) + require.Len(t, message.Content, 1) + content = &message.Content[0] + } + + require.NotNil(t, content) + require.Equal(t, "admin", content.Text) + }) + } + }) } func TestOpenAIChatCompletions(t *testing.T) { t.Parallel() - sessionToken := getSessionToken(t) + client := coderdtest.New(t, nil) + sessionToken := getSessionToken(t, client) t.Run("single builtin tool", func(t *testing.T) { t.Parallel() @@ -197,7 +334,7 @@ func TestOpenAIChatCompletions(t *testing.T) { reqBody = newBody ctx := testutil.Context(t, testutil.WaitLong) - srv := newMockServer(ctx, t, files) + srv := newMockServer(ctx, t, files, nil) t.Cleanup(srv.Close) coderdClient := &fakeBridgeDaemonClient{} @@ -260,7 +397,8 @@ func TestOpenAIChatCompletions(t *testing.T) { func TestSimple(t *testing.T) { t.Parallel() - sessionToken := getSessionToken(t) + client := coderdtest.New(t, nil) + sessionToken := getSessionToken(t, client) testCases := []struct { name string @@ -337,7 +475,7 @@ func TestSimple(t *testing.T) { // Given: a mock API server and a Bridge through which the requests will flow. ctx := testutil.Context(t, testutil.WaitLong) - srv := newMockServer(ctx, t, files) + srv := newMockServer(ctx, t, files, nil) t.Cleanup(srv.Close) coderdClient := &fakeBridgeDaemonClient{} @@ -418,10 +556,9 @@ func createOpenAIChatCompletionsReq(t *testing.T, baseURL string, input []byte) return req } -func getSessionToken(t *testing.T) string { +func getSessionToken(t *testing.T, client *codersdk.Client) string { t.Helper() - client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) resp, err := client.LoginWithPassword(t.Context(), codersdk.LoginWithPasswordRequest{ Email: coderdtest.FirstUserParams.Email, @@ -434,11 +571,18 @@ func getSessionToken(t *testing.T) string { type mockServer struct { *httptest.Server + + callCount atomic.Uint32 } -func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap) *mockServer { +func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap, responseMutatorFn func(reqCount uint32, resp []byte) []byte) *mockServer { t.Helper() + + ms := &mockServer{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callCount := ms.callCount.Add(1) + t.Logf("\n\nCALL COUNT: %d\n\n", callCount) + body, err := io.ReadAll(r.Body) defer r.Body.Close() require.NoError(t, err) @@ -450,9 +594,14 @@ func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap) *moc require.NoError(t, json.Unmarshal(body, &reqMsg)) if !reqMsg.Stream { + resp := files[fixtureNonStreamingResponse] + if responseMutatorFn != nil { + resp = responseMutatorFn(ms.callCount.Load(), resp) + } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(files[fixtureNonStreamingResponse]) + w.Write(resp) return } @@ -461,7 +610,12 @@ func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap) *moc w.Header().Set("Connection", "keep-alive") w.Header().Set("Access-Control-Allow-Origin", "*") - scanner := bufio.NewScanner(bytes.NewReader(files[fixtureStreamingResponse])) + resp := files[fixtureStreamingResponse] + if responseMutatorFn != nil { + resp = responseMutatorFn(ms.callCount.Load(), resp) + } + + scanner := bufio.NewScanner(bytes.NewReader(resp)) flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming unsupported", http.StatusInternalServerError) @@ -480,14 +634,12 @@ func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap) *moc return } })) - srv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx } - return &mockServer{ - Server: srv, - } + ms.Server = srv + return ms } type fakeBridgeDaemonClient struct { @@ -526,3 +678,25 @@ func (f *fakeBridgeDaemonClient) TrackToolUsage(ctx context.Context, in *proto.T return &proto.TrackToolUsageResponse{}, nil } + +func createMockMCPSrv(t *testing.T) http.Handler { + t.Helper() + + s := server.NewMCPServer( + "Mock coder MCP server", + "1.0.0", + server.WithToolCapabilities(true), + ) + + // Add tool + tool := mcp.NewTool("coder_get_authenticated_user", + mcp.WithDescription("Mock of the coder_get_authenticated_user tool"), + ) + + // Add tool handler + s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return mcp.NewToolResultText("mock"), nil + }) + + return server.NewStreamableHTTPServer(s) +} diff --git a/aibridged/fixtures/anthropic/single_injected_tool.txtar b/aibridged/fixtures/anthropic/single_injected_tool.txtar new file mode 100644 index 0000000000000..4c29b7c7f9a3c --- /dev/null +++ b/aibridged/fixtures/anthropic/single_injected_tool.txtar @@ -0,0 +1,125 @@ +Coder MCP tools automatically injected. + +-- request -- +{ + "model": "claude-sonnet-4-20250514", + "max_tokens": 1024, + "messages": [ + { + "role": "user", + "content": "what is my coder username" + } + ] +} + +-- streaming -- +event: message_start +data: {"type":"message_start","message":{"id":"msg_01FHxVHRpKWz6dFfm9s1jkNY","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":385,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5,"service_tier":"standard"}} } + +event: content_block_start +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + +event: ping +data: {"type": "ping"} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll check your"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Coder username for you."} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: content_block_start +data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_018gpsLYxqC9D7v4nuJL5Qvy","name":"__mcp__coder_coder_get_authenticated_user","input":{}} } + +event: content_block_delta +data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} } + +event: content_block_stop +data: {"type":"content_block_stop","index":1 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":59} } + +event: message_stop +data: {"type":"message_stop" } + + +-- streaming/tool-call -- +event: message_start +data: {"type":"message_start","message":{"id":"msg_01Rsww53iPc1nW2uEBrMkxH6","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1162,"cache_read_input_tokens":35876,"output_tokens":1,"service_tier":"standard"}} } + +event: content_block_start +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + +event: ping +data: {"type": "ping"} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"admin"} } + +event: content_block_stop +data: {"type":"content_block_stop","index":0 } + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":4} } + +event: message_stop +data: {"type":"message_stop" } + + +-- non-streaming -- +{ + "id": "msg_018NqsW4eMw8tw47xBby5sV6", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-20250514", + "content": [ + { + "type": "text", + "text": "I'll check your current Coder username for you." + }, + { + "type": "tool_use", + "id": "toolu_01PNPxdk8GqP9uTpWH7C2wu1", + "name": "__mcp__coder_coder_get_authenticated_user", + "input": {} + } + ], + "stop_reason": "tool_use", + "stop_sequence": null, + "usage": { + "input_tokens": 385, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 60, + "service_tier": "standard" + } +} + + +-- non-streaming/tool-call -- +{ + "id": "msg_01H7amafFFU8rkqPNNX7LoZ2", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-20250514", + "content": [ + { + "type": "text", + "text": "admin" + } + ], + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 7, + "cache_creation_input_tokens": 1175, + "cache_read_input_tokens": 0, + "output_tokens": 12, + "service_tier": "standard" + } +} + From 5c3469e696e2020334b6b38dd014d001ac73584f Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 5 Aug 2025 13:03:35 +0200 Subject: [PATCH 47/61] OpenAI tool call test, overriding IDs of returned messages Signed-off-by: Danny Kopping --- aibridged/bridge.go | 161 +++++++++++----- aibridged/bridge_test.go | 136 ++++++++++++- aibridged/fixtures/README.md | 25 +++ .../anthropic/single_injected_tool.txtar | 10 +- .../openai/single_injected_tool.txtar | 158 ++++++++++++++++ aibridged/proto/aibridged.pb.go | 179 +++++++++++------- aibridged/proto/aibridged.proto | 27 +-- aibridged/streaming.go | 5 +- cli/testdata/server-config.yaml.golden | 14 +- 9 files changed, 579 insertions(+), 136 deletions(-) create mode 100644 aibridged/fixtures/README.md create mode 100644 aibridged/fixtures/openai/single_injected_tool.txtar diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 2bd9fadea62b8..cc4f9374af7c3 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -129,7 +129,7 @@ func (b *Bridge) Handler() http.Handler { // References: // - https://platform.openai.com/docs/api-reference/chat-streaming func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { - sessionID := uuid.New() + sessionID := uuid.NewString() b.logger.Info(r.Context(), "openai request started", slog.F("session_id", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) @@ -171,8 +171,10 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { prompt, err := in.LastUserPrompt() // TODO: error handling. if prompt != nil { if _, err = coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ - Model: in.Model, - Prompt: *prompt, + SessionId: sessionID, + MsgId: "", // Not yet known. We could move this after the response, but probably better to track prospectively. + Model: in.Model, + Prompt: *prompt, }); err != nil { b.logger.Error(r.Context(), "failed to track user prompt", slog.Error(err)) } @@ -239,7 +241,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } }() - BasicSSESender(streamCtx, sessionID, "", events, b.logger.Named("sse-sender")).ServeHTTP(w, r) + BasicSSESender(streamCtx, events, b.logger.Named("sse-sender")).ServeHTTP(w, r) }() // TODO: implement parallel tool calls. @@ -270,9 +272,11 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { shouldRelayChunk = false } else { _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - Model: string(in.Model), - Input: toolCall.Arguments, - Tool: toolCall.Name, + SessionId: sessionID, + MsgId: chunk.ID, + Model: string(in.Model), + Input: toolCall.Arguments, + Tool: toolCall.Name, }) if err != nil { b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) @@ -293,15 +297,18 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { chunk.Usage = cumulativeUsage } + // Overwrite response identifier since proxy obscures injected tool call invocations. + chunk.ID = sessionID events.TrySend(ctx, chunk) - fmt.Printf("\t[out]: %s\n", chunk.RawJSON()) + // fmt.Printf("\t[out]: %s\n", chunk.RawJSON()) } } // If the usage information is set, track it. // The API will send usage information when the response terminates, which will happen if a tool call is invoked. if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + SessionId: sessionID, MsgId: acc.ID, Model: string(acc.Model), InputTokens: cumulativeUsage.PromptTokens, @@ -323,6 +330,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { var apierr *openai.Error if errors.As(err, &apierr) { events.TrySend(ctx, map[string]interface{}{ + // TODO: session ID? "error": true, "message": err.Error(), }) @@ -354,10 +362,12 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - Model: string(in.Model), - Input: tc.Arguments, - Tool: tc.Name, - Injected: true, + SessionId: sessionID, + MsgId: acc.ID, + Model: string(in.Model), + Input: tc.Arguments, + Tool: tc.Name, // TODO: sanitize tool name. + Injected: true, }) if err != nil { b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) @@ -372,6 +382,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { if err != nil { // Always provide a tool_result even if the tool call failed errorResponse := map[string]interface{}{ + // TODO: session ID? "error": true, "message": err.Error(), } @@ -386,6 +397,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // Always provide a tool_result even if encoding failed // TODO: abstract. errorResponse := map[string]interface{}{ + // TODO: session ID? "error": true, "message": err.Error(), } @@ -418,9 +430,13 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } else { // Non-streaming case with tool calling support var cumulativeUsage openai.CompletionUsage + var ( + completion *openai.ChatCompletion + err error + ) for { - completion, err := client.Chat.Completions.New(ctx, req) + completion, err = client.Chat.Completions.New(ctx, req) if err != nil { if isConnectionError(err) { b.logger.Debug(ctx, "upstream connection closed", slog.Error(err)) @@ -442,6 +458,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // Track token usage if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + SessionId: sessionID, MsgId: completion.ID, Model: string(completion.Model), InputTokens: cumulativeUsage.PromptTokens, @@ -466,9 +483,11 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { pendingToolCalls = append(pendingToolCalls, toolCall) } else { _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - Model: string(in.Model), - Input: toolCall.Function.Arguments, - Tool: toolCall.Function.Name, + SessionId: sessionID, + MsgId: completion.ID, + Model: string(in.Model), + Input: toolCall.Function.Arguments, + Tool: toolCall.Function.Name, }) if err != nil { b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) @@ -479,14 +498,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // If no injected tool calls, we're done if len(pendingToolCalls) == 0 { - // Update the cumulative usage in the final response - if completion.Usage.CompletionTokens > 0 { - completion.Usage = cumulativeUsage - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(completion.RawJSON())) break } @@ -506,10 +517,12 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { } _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - Model: string(in.Model), - Input: tc.Function.Arguments, - Tool: fn, - Injected: true, + SessionId: sessionID, + MsgId: completion.ID, + Model: string(in.Model), + Input: tc.Function.Arguments, + Tool: fn, // TODO: sanitize tool name. + Injected: true, }) if err != nil { b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) @@ -525,6 +538,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { if err != nil { // Always provide a tool result even if the tool call failed errorResponse := map[string]interface{}{ + // TODO: session ID? "error": true, "message": err.Error(), } @@ -538,6 +552,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) // Always provide a tool result even if encoding failed errorResponse := map[string]interface{}{ + // TODO: session ID? "error": true, "message": err.Error(), } @@ -549,6 +564,35 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { req.Messages = append(req.Messages, openai.ToolMessage(out.String(), tc.ID)) } } + + if completion == nil { + return + } + + // Overwrite response identifier since proxy obscures injected tool call invocations. + completion.ID = sessionID + + // Update the cumulative usage in the final response + if completion.Usage.CompletionTokens > 0 { + completion.Usage = cumulativeUsage + } + + w.Header().Set("Content-Type", "application/json") + out, err := json.Marshal(completion) + if err != nil { + // TODO: abstract. + errorResponse := map[string]interface{}{ + // TODO: session ID? + "error": true, + "message": fmt.Sprintf("failed to marshal response: %s", err), + } + out, _ = json.Marshal(errorResponse) + w.WriteHeader(http.StatusInternalServerError) + } else { + w.WriteHeader(http.StatusOK) + } + + _, _ = w.Write(out) } } @@ -578,7 +622,7 @@ func LoggingMiddleware(req *http.Request, next option.MiddlewareNext) (res *http // TODO: track cumulative usage when tool invocations are executed; see OpenAI implementation. func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { - sessionID := uuid.New() + sessionID := uuid.NewString() b.logger.Info(r.Context(), "anthropic request started", slog.F("session_id", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) @@ -679,6 +723,8 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). + // It's highly unlikely that operators want to see these prompts tracked, but the token usage must be. + // We could consider making this configurable in the future. isSmallFastModel := strings.Contains(string(in.Model), "3-5-haiku") // Find the most recent user message and track the prompt. @@ -686,8 +732,10 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { prompt, err := in.LastUserPrompt() // TODO: error handling. if prompt != nil { if _, err = coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ - Prompt: *prompt, - Model: string(in.Model), + SessionId: sessionID, + MsgId: "", // Not yet known. We could move this after the response, but probably better to track prospectively. + Prompt: *prompt, + Model: string(in.Model), }); err != nil { b.logger.Error(r.Context(), "failed to track user prompt", slog.Error(err)) } @@ -721,6 +769,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") client := anthropic.NewClient(opts...) if !in.UseStreaming() { + opts = append(opts, option.WithRequestTimeout(time.Second*30)) // TODO: configurable. for { resp, err := client.Beta.Messages.New(ctx, messages, opts...) if err != nil { @@ -734,9 +783,14 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { http.Error(w, antErr.Error.Message, antErr.StatusCode) return } + + b.logger.Error(ctx, "upstream API error", slog.Error(err)) + http.Error(w, "internal error", http.StatusInternalServerError) + return } if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + SessionId: sessionID, MsgId: resp.ID, Model: string(resp.Model), InputTokens: resp.Usage.InputTokens, @@ -768,9 +822,11 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // If tool is not injected, track it since the client will be handling it. if serialized, err := json.Marshal(toolUse.Input); err == nil { _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - Model: string(resp.Model), - Input: string(serialized), - Tool: toolUse.Name, + SessionId: sessionID, + MsgId: resp.ID, + Model: string(resp.Model), + Input: string(serialized), + Tool: toolUse.Name, }) if err != nil { b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) @@ -782,6 +838,9 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // If no injected tool calls, we're done. if len(pendingToolCalls) == 0 { + // Overwrite response identifier since proxy obscures injected tool call invocations. + resp.ID = sessionID + out, err := json.Marshal(resp) if err != nil { http.Error(w, "error marshaling response", http.StatusInternalServerError) @@ -821,10 +880,12 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - Model: string(resp.Model), - Input: string(serialized), - Tool: tc.Name, - Injected: true, + SessionId: sessionID, + MsgId: resp.ID, + Model: string(resp.Model), + Input: string(serialized), + Tool: tc.Name, // TODO: sanitize tool name. + Injected: true, }) if err != nil { b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) @@ -933,7 +994,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } }() - BasicSSESender(streamCtx, sessionID, string(in.Model), es, b.logger.Named("sse-sender")).ServeHTTP(w, r) + BasicSSESender(streamCtx, es, b.logger.Named("sse-sender")).ServeHTTP(w, r) }() isFirst := true @@ -989,6 +1050,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // See https://docs.anthropic.com/en/docs/build-with-claude/streaming#event-types. start := event.AsMessageStart() if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ + SessionId: sessionID, MsgId: message.ID, Model: string(message.Model), InputTokens: start.Message.Usage.InputTokens, @@ -1013,6 +1075,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): delta := event.AsMessageDelta() if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ + SessionId: sessionID, MsgId: message.ID, Model: string(message.Model), InputTokens: delta.Usage.InputTokens, @@ -1081,10 +1144,12 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if serialized, err := json.Marshal(input); err == nil { _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ - Model: string(message.Model), - Input: string(serialized), - Tool: tool.Name, - Injected: true, + SessionId: sessionID, + MsgId: message.ID, + Model: string(message.Model), + Input: string(serialized), + Tool: tool.Name, // TODO: sanitize tool name. + Injected: true, }) if err != nil { b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) @@ -1229,9 +1294,11 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if serialized, err := json.Marshal(variant.Input); err == nil { _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ - Model: string(message.Model), - Input: string(serialized), - Tool: variant.Name, + SessionId: sessionID, + MsgId: message.ID, + Model: string(message.Model), + Input: string(serialized), + Tool: variant.Name, }) if err != nil { b.logger.Error(ctx, "failed to track non-injected tool usage", slog.Error(err)) @@ -1244,6 +1311,8 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } + // Overwrite response identifier since proxy obscures injected tool call invocations. + event.Message.ID = sessionID if err := es.TrySend(streamCtx, event); err != nil { b.logConnectionError(ctx, err, "sending event") if isConnectionError(err) { diff --git a/aibridged/bridge_test.go b/aibridged/bridge_test.go index efa5e68b2bd18..4d81716d443cb 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_test.go @@ -24,6 +24,9 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/sjson" + "github.com/openai/openai-go" + oai_ssestream "github.com/openai/openai-go/packages/ssestream" + "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -44,6 +47,8 @@ var ( //go:embed fixtures/openai/single_builtin_tool.txtar oaiSingleBuiltinTool []byte + //go:embed fixtures/openai/single_injected_tool.txtar + oaiSingleInjectedTool []byte //go:embed fixtures/anthropic/simple.txtar antSimple []byte @@ -162,6 +167,7 @@ func TestAnthropicMessages(t *testing.T) { } }) + // TODO: fixture contains hardcoded tool name; this is flimsy since our naming convention may change for injected tools. t.Run("single injected tool", func(t *testing.T) { t.Parallel() @@ -392,6 +398,133 @@ func TestOpenAIChatCompletions(t *testing.T) { }) } }) + + // TODO: fixture contains hardcoded tool name; this is flimsy since our naming convention may change for injected tools. + t.Run("single injected tool", func(t *testing.T) { + t.Parallel() + + cases := []struct { + streaming bool + }{ + { + streaming: true, + }, + // { + // streaming: false, + // }, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("%s/streaming=%v", t.Name(), tc.streaming), func(t *testing.T) { + t.Parallel() + + arc := txtar.Parse(oaiSingleInjectedTool) + t.Logf("%s: %s", t.Name(), arc.Comment) + + files := filesMap(arc) + require.Len(t, files, 5) + require.Contains(t, files, fixtureRequest) + require.Contains(t, files, fixtureStreamingResponse) + require.Contains(t, files, fixtureNonStreamingResponse) + require.Contains(t, files, fixtureStreamingToolResponse) + require.Contains(t, files, fixtureNonStreamingToolResponse) + + reqBody := files[fixtureRequest] + + // Add the stream param to the request. + newBody, err := sjson.SetBytes(reqBody, "stream", tc.streaming) + require.NoError(t, err) + reqBody = newBody + + ctx := testutil.Context(t, testutil.WaitLong) + // Conditionally return fixtures based on request count. + // First request: halts with tool call instruction. + // Second request: tool call invocation. + mockSrv := newMockServer(ctx, t, files, func(reqCount uint32, resp []byte) []byte { + if reqCount == 1 { + return resp + } + + if reqCount > 2 { + t.Fatalf("did not expect more than 2 calls; received %d", reqCount) + } + + if !tc.streaming { + return files[fixtureNonStreamingToolResponse] + } + return files[fixtureStreamingToolResponse] + }) + t.Cleanup(mockSrv.Close) + + coderdClient := &fakeBridgeDaemonClient{} + logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + + // Setup Coder MCP integration. + mcpSrv := httptest.NewServer(createMockMCPSrv(t)) + mcpBridge, err := aibridged.NewMCPToolBridge("coder", mcpSrv.URL, map[string]string{}, logger) + require.NoError(t, err) + + // Initialize MCP client, fetch tools, and inject into bridge. + require.NoError(t, mcpBridge.Init(testutil.Context(t, testutil.WaitShort))) + tools := mcpBridge.ListTools() + require.NotEmpty(t, tools) + + b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + OpenAI: codersdk.AIBridgeOpenAIConfig{ + BaseURL: serpent.String(mockSrv.URL), + Key: serpent.String(sessionToken), + }, + }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return coderdClient, true + }, tools) + require.NoError(t, err) + + // Invoke request to mocked API via aibridge. + bridgeSrv := httptest.NewServer(b.Handler()) + req := createOpenAIChatCompletionsReq(t, bridgeSrv.URL, reqBody) + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() + + // We must ALWAYS have 2 calls to the bridge. + require.Eventually(t, func() bool { return mockSrv.callCount.Load() == 2 }, testutil.WaitLong, testutil.IntervalFast) + + // TODO: this is a bit flimsy since this API won't be in beta forever. + var content *openai.ChatCompletionChoice + if tc.streaming { + // Parse the response stream. + decoder := oai_ssestream.NewDecoder(resp) + stream := oai_ssestream.NewStream[openai.ChatCompletionChunk](decoder, nil) + var message openai.ChatCompletionAccumulator + for stream.Next() { + chunk := stream.Current() + message.AddChunk(chunk) + } + + require.NoError(t, stream.Err()) + require.Len(t, message.Choices, 1) + content = &message.Choices[0] + } else { + // Parse & unmarshal the response. + out, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + // TODO: this is a bit flimsy since this API won't be in beta forever. + var message openai.ChatCompletion + require.NoError(t, json.Unmarshal(out, &message)) + require.NotNil(t, message) + require.Len(t, message.Choices, 1) + content = &message.Choices[0] + } + + require.NotNil(t, content) + require.Contains(t, content.Message.Content, "admin") + }) + } + }) } func TestSimple(t *testing.T) { @@ -580,8 +713,7 @@ func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap, resp ms := &mockServer{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - callCount := ms.callCount.Add(1) - t.Logf("\n\nCALL COUNT: %d\n\n", callCount) + ms.callCount.Add(1) body, err := io.ReadAll(r.Body) defer r.Body.Close() diff --git a/aibridged/fixtures/README.md b/aibridged/fixtures/README.md new file mode 100644 index 0000000000000..075eaed0a3253 --- /dev/null +++ b/aibridged/fixtures/README.md @@ -0,0 +1,25 @@ +These fixtures were created by adding logging middleware to API calls to view the raw requests/responses. + +```go +... +opts = append(opts, option.WithMiddleware(LoggingMiddleware)) +... + +func LoggingMiddleware(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { + reqOut, _ := httputil.DumpRequest(req, true) + + // Forward the request to the next handler + res, err = next(req) + fmt.Printf("[req] %s\n", reqOut) + + // Handle stuff after the request + if err != nil { + return res, err + } + + respOut, _ := httputil.DumpResponse(res, true) + fmt.Printf("[resp] %s\n", respOut) + + return res, err +} +``` diff --git a/aibridged/fixtures/anthropic/single_injected_tool.txtar b/aibridged/fixtures/anthropic/single_injected_tool.txtar index 4c29b7c7f9a3c..c5c4ce249119f 100644 --- a/aibridged/fixtures/anthropic/single_injected_tool.txtar +++ b/aibridged/fixtures/anthropic/single_injected_tool.txtar @@ -3,7 +3,7 @@ Coder MCP tools automatically injected. -- request -- { "model": "claude-sonnet-4-20250514", - "max_tokens": 1024, + "max_tokens": 1, "messages": [ { "role": "user", @@ -14,7 +14,7 @@ Coder MCP tools automatically injected. -- streaming -- event: message_start -data: {"type":"message_start","message":{"id":"msg_01FHxVHRpKWz6dFfm9s1jkNY","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":385,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5,"service_tier":"standard"}} } +data: {"type":"message_start","message":{"id":"1545b825-1943-43e6-90e0-2d76b7b51afd","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":385,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5,"service_tier":"standard"}} } event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } @@ -49,7 +49,7 @@ data: {"type":"message_stop" } -- streaming/tool-call -- event: message_start -data: {"type":"message_start","message":{"id":"msg_01Rsww53iPc1nW2uEBrMkxH6","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1162,"cache_read_input_tokens":35876,"output_tokens":1,"service_tier":"standard"}} } +data: {"type":"message_start","message":{"id":"0a2b5ae7-4973-41d1-b725-d724e7d85408","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1162,"cache_read_input_tokens":35876,"output_tokens":1,"service_tier":"standard"}} } event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } @@ -72,7 +72,7 @@ data: {"type":"message_stop" } -- non-streaming -- { - "id": "msg_018NqsW4eMw8tw47xBby5sV6", + "id": "e3d937ce-d871-4b01-9c9b-c20227c27553", "type": "message", "role": "assistant", "model": "claude-sonnet-4-20250514", @@ -102,7 +102,7 @@ data: {"type":"message_stop" } -- non-streaming/tool-call -- { - "id": "msg_01H7amafFFU8rkqPNNX7LoZ2", + "id": "7f7f66fa-fbad-48ea-80dc-31d4a05debcc", "type": "message", "role": "assistant", "model": "claude-sonnet-4-20250514", diff --git a/aibridged/fixtures/openai/single_injected_tool.txtar b/aibridged/fixtures/openai/single_injected_tool.txtar new file mode 100644 index 0000000000000..b5013e65dcae5 --- /dev/null +++ b/aibridged/fixtures/openai/single_injected_tool.txtar @@ -0,0 +1,158 @@ +Coder MCP tools automatically injected. + +-- request -- +{ + "model": "gpt-4.1", + "messages": [ + { + "role": "user", + "content": "what is my coder username" + } + ] +} + +-- streaming -- +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" am"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" retrieve"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" your"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" C"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"oder"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" username"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" by"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" checking"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" currently"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" authenticated"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_ksPDU5jx3nxrlTbSGXRNdMqN","type":"function","function":{"name":"__mcp__coder_coder_get_authenticated_user","arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{}"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} + +data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":4860,"completion_tokens":35,"total_tokens":4895,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + +data: [DONE] + + +-- streaming/tool-call -- +data: {"id":"6b0e7f1b-e75f-4661-8fb5-d3ecdae7ead0","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"6b0e7f1b-e75f-4661-8fb5-d3ecdae7ead0","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"admin"},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"6b0e7f1b-e75f-4661-8fb5-d3ecdae7ead0","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} + +data: {"id":"6b0e7f1b-e75f-4661-8fb5-d3ecdae7ead0","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":5093,"completion_tokens":1,"total_tokens":5094,"prompt_tokens_details":{"cached_tokens":4864,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + +data: [DONE] + + +-- non-streaming -- +{ + "id": "0d736322-1f3a-4748-9587-2a88dc42c664", + "object": "chat.completion", + "created": 1754318705, + "model": "gpt-4.1-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "I am about to retrieve your Coder username by querying your authenticated user information.", + "tool_calls": [ + { + "id": "call_O0miYTlPRzYjDf912pi3Ua1t", + "type": "function", + "function": { + "name": "__mcp__coder_coder_get_authenticated_user", + "arguments": "{}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 4860, + "completion_tokens": 35, + "total_tokens": 4895, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_799e4ca3f1" +} + + +-- non-streaming/tool-call -- +{ + "id": "496bca0d-c6bd-4607-89a5-169b4affa4f2", + "object": "chat.completion", + "created": 1754318706, + "model": "gpt-4.1-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "admin", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 5093, + "completion_tokens": 1, + "total_tokens": 5094, + "prompt_tokens_details": { + "cached_tokens": 4864, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_799e4ca3f1" +} + diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index e3044adc59898..90a5391db5619 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -25,11 +25,12 @@ type TrackTokenUsageRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - MsgId string `protobuf:"bytes,1,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` - InputTokens int64 `protobuf:"varint,2,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` - OutputTokens int64 `protobuf:"varint,3,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` - Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` - Other map[string]int64 `protobuf:"bytes,5,rep,name=other,proto3" json:"other,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` + InputTokens int64 `protobuf:"varint,3,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` + OutputTokens int64 `protobuf:"varint,4,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` + Model string `protobuf:"bytes,5,opt,name=model,proto3" json:"model,omitempty"` + Other map[string]int64 `protobuf:"bytes,6,rep,name=other,proto3" json:"other,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` } func (x *TrackTokenUsageRequest) Reset() { @@ -64,6 +65,13 @@ func (*TrackTokenUsageRequest) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} } +func (x *TrackTokenUsageRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + func (x *TrackTokenUsageRequest) GetMsgId() string { if x != nil { return x.MsgId @@ -142,8 +150,10 @@ type TrackUserPromptRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Prompt string `protobuf:"bytes,1,opt,name=prompt,proto3" json:"prompt,omitempty"` - Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` + Prompt string `protobuf:"bytes,3,opt,name=prompt,proto3" json:"prompt,omitempty"` + Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` } func (x *TrackUserPromptRequest) Reset() { @@ -178,6 +188,20 @@ func (*TrackUserPromptRequest) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} } +func (x *TrackUserPromptRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *TrackUserPromptRequest) GetMsgId() string { + if x != nil { + return x.MsgId + } + return "" +} + func (x *TrackUserPromptRequest) GetPrompt() string { if x != nil { return x.Prompt @@ -235,10 +259,12 @@ type TrackToolUsageRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Tool string `protobuf:"bytes,1,opt,name=tool,proto3" json:"tool,omitempty"` - Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` - Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` - Injected bool `protobuf:"varint,4,opt,name=injected,proto3" json:"injected,omitempty"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` + Tool string `protobuf:"bytes,3,opt,name=tool,proto3" json:"tool,omitempty"` + Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` + Input string `protobuf:"bytes,5,opt,name=input,proto3" json:"input,omitempty"` + Injected bool `protobuf:"varint,6,opt,name=injected,proto3" json:"injected,omitempty"` } func (x *TrackToolUsageRequest) Reset() { @@ -273,6 +299,20 @@ func (*TrackToolUsageRequest) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{4} } +func (x *TrackToolUsageRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *TrackToolUsageRequest) GetMsgId() string { + if x != nil { + return x.MsgId + } + return "" +} + func (x *TrackToolUsageRequest) GetTool() string { if x != nil { return x.Tool @@ -344,62 +384,71 @@ var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x0a, 0x1f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x09, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x22, 0x8b, 0x02, 0x0a, + 0x6f, 0x12, 0x09, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x22, 0xaa, 0x02, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, - 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x42, 0x0a, 0x05, - 0x6f, 0x74, 0x68, 0x65, 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x69, - 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, - 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x6f, 0x74, 0x68, 0x65, 0x72, - 0x1a, 0x38, 0x0a, 0x0a, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, - 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x46, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, - 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x19, 0x0a, - 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x73, 0x0a, 0x15, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, - 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, - 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x18, 0x0a, - 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x9b, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, - 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, - 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, - 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, 0x0a, + 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x42, 0x0a, 0x05, 0x6f, + 0x74, 0x68, 0x65, 0x72, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x69, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, 0x74, + 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x1a, + 0x38, 0x0a, 0x0a, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, - 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, - 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, - 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, - 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, - 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, - 0x0a, 0x0e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x12, 0x20, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, - 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, - 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, - 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x7c, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, + 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, + 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, + 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, + 0x73, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, + 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, + 0x65, 0x6c, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, + 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa9, 0x01, + 0x0a, 0x15, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x12, 0x0a, + 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, + 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, + 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x32, 0x9b, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, + 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, + 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, + 0x6d, 0x70, 0x74, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, + 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, + 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, + 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, + 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, + 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, + 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index 324b3427d88e4..a3d9b0aca3f39 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -4,25 +4,30 @@ option go_package = "github.com/coder/coder/v2/aibridged/proto"; package aibridged; message TrackTokenUsageRequest { - string msg_id = 1; - int64 input_tokens = 2; - int64 output_tokens = 3; - string model = 4; - map other = 5; + string session_id = 1; + string msg_id = 2; + int64 input_tokens = 3; + int64 output_tokens = 4; + string model = 5; + map other = 6; } message TrackTokenUsageResponse {} message TrackUserPromptRequest { - string prompt = 1; - string model = 2; + string session_id = 1; + string msg_id = 2; + string prompt = 3; + string model = 4; } message TrackUserPromptResponse {} message TrackToolUsageRequest { - string tool = 1; - string model = 2; - string input = 3; - bool injected = 4; + string session_id = 1; + string msg_id = 2; + string tool = 3; + string model = 4; + string input = 5; + bool injected = 6; } message TrackToolUsageResponse {} diff --git a/aibridged/streaming.go b/aibridged/streaming.go index abb4336ef270c..23b8c4fdd907d 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -12,7 +12,6 @@ import ( "sync" "syscall" - "github.com/google/uuid" "golang.org/x/xerrors" "cdr.dev/slog" @@ -40,7 +39,7 @@ func isConnectionError(err error) bool { // BasicSSESender was implemented to overcome httpapi.ServerSentEventSender's odd design choices. For example, it doesn't // write "event: data" for every data event (it's unnecessary, and breaks some AI tools' parsing of the SSE stream). -func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, model string, stream EventStreamer, logger slog.Logger) http.HandlerFunc { +func BasicSSESender(outerCtx context.Context, stream EventStreamer, logger slog.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -57,7 +56,7 @@ func BasicSSESender(outerCtx context.Context, sessionID uuid.UUID, model string, case <-outerCtx.Done(): return case <-ctx.Done(): - fmt.Printf("request done for model %s, reason: %q\n", model, ctx.Err()) + fmt.Printf("request done, reason: %q\n", ctx.Err()) return case <-stream.Closed(): return diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 33078d7835ee0..576e16817b298 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -714,8 +714,14 @@ ai_bridge: # (default: 3, type: int) daemons: 3 # TODO. - # (default: https://api.openai.com, type: string) - daemons: https://api.openai.com + # (default: https://api.openai.com/v1/, type: string) + openai_base_url: https://api.openai.com/v1/ # TODO. - # (default: https://api.anthropic.com, type: string) - daemons: https://api.anthropic.com + # (default: , type: string) + openai_key: "" + # TODO. + # (default: https://api.anthropic.com/, type: string) + base_url: https://api.anthropic.com/ + # TODO. + # (default: , type: string) + key: "" From 3fc6d0952328c52acc25ba30a84a444f6b77bdf8 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 5 Aug 2025 13:46:49 +0200 Subject: [PATCH 48/61] Reverting UUIDs in fixtures; this was incorrect Signed-off-by: Danny Kopping --- .../anthropic/single_injected_tool.txtar | 10 ++-- .../openai/single_injected_tool.txtar | 54 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/aibridged/fixtures/anthropic/single_injected_tool.txtar b/aibridged/fixtures/anthropic/single_injected_tool.txtar index c5c4ce249119f..4c29b7c7f9a3c 100644 --- a/aibridged/fixtures/anthropic/single_injected_tool.txtar +++ b/aibridged/fixtures/anthropic/single_injected_tool.txtar @@ -3,7 +3,7 @@ Coder MCP tools automatically injected. -- request -- { "model": "claude-sonnet-4-20250514", - "max_tokens": 1, + "max_tokens": 1024, "messages": [ { "role": "user", @@ -14,7 +14,7 @@ Coder MCP tools automatically injected. -- streaming -- event: message_start -data: {"type":"message_start","message":{"id":"1545b825-1943-43e6-90e0-2d76b7b51afd","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":385,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5,"service_tier":"standard"}} } +data: {"type":"message_start","message":{"id":"msg_01FHxVHRpKWz6dFfm9s1jkNY","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":385,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5,"service_tier":"standard"}} } event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } @@ -49,7 +49,7 @@ data: {"type":"message_stop" } -- streaming/tool-call -- event: message_start -data: {"type":"message_start","message":{"id":"0a2b5ae7-4973-41d1-b725-d724e7d85408","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1162,"cache_read_input_tokens":35876,"output_tokens":1,"service_tier":"standard"}} } +data: {"type":"message_start","message":{"id":"msg_01Rsww53iPc1nW2uEBrMkxH6","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1162,"cache_read_input_tokens":35876,"output_tokens":1,"service_tier":"standard"}} } event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } @@ -72,7 +72,7 @@ data: {"type":"message_stop" } -- non-streaming -- { - "id": "e3d937ce-d871-4b01-9c9b-c20227c27553", + "id": "msg_018NqsW4eMw8tw47xBby5sV6", "type": "message", "role": "assistant", "model": "claude-sonnet-4-20250514", @@ -102,7 +102,7 @@ data: {"type":"message_stop" } -- non-streaming/tool-call -- { - "id": "7f7f66fa-fbad-48ea-80dc-31d4a05debcc", + "id": "msg_01H7amafFFU8rkqPNNX7LoZ2", "type": "message", "role": "assistant", "model": "claude-sonnet-4-20250514", diff --git a/aibridged/fixtures/openai/single_injected_tool.txtar b/aibridged/fixtures/openai/single_injected_tool.txtar index b5013e65dcae5..2a1f96696ee26 100644 --- a/aibridged/fixtures/openai/single_injected_tool.txtar +++ b/aibridged/fixtures/openai/single_injected_tool.txtar @@ -12,66 +12,66 @@ Coder MCP tools automatically injected. } -- streaming -- -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" am"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" am"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" retrieve"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" retrieve"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" your"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" your"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" C"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" C"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"oder"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"oder"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" username"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" username"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" by"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" by"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" checking"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" checking"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" currently"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" currently"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" authenticated"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" authenticated"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_ksPDU5jx3nxrlTbSGXRNdMqN","type":"function","function":{"name":"__mcp__coder_coder_get_authenticated_user","arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_ksPDU5jx3nxrlTbSGXRNdMqN","type":"function","function":{"name":"__mcp__coder_coder_get_authenticated_user","arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{}"}}]},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{}"}}]},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} -data: {"id":"2ccc4d44-949c-485f-9e72-c87bb87e934e","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":4860,"completion_tokens":35,"total_tokens":4895,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} +data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":4860,"completion_tokens":35,"total_tokens":4895,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] -- streaming/tool-call -- -data: {"id":"6b0e7f1b-e75f-4661-8fb5-d3ecdae7ead0","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkci2O8GBHoOT04PukNschUStOw","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"6b0e7f1b-e75f-4661-8fb5-d3ecdae7ead0","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"admin"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C0qkci2O8GBHoOT04PukNschUStOw","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"admin"},"logprobs":null,"finish_reason":null}],"usage":null} -data: {"id":"6b0e7f1b-e75f-4661-8fb5-d3ecdae7ead0","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} +data: {"id":"chatcmpl-C0qkci2O8GBHoOT04PukNschUStOw","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} -data: {"id":"6b0e7f1b-e75f-4661-8fb5-d3ecdae7ead0","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":5093,"completion_tokens":1,"total_tokens":5094,"prompt_tokens_details":{"cached_tokens":4864,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} +data: {"id":"chatcmpl-C0qkci2O8GBHoOT04PukNschUStOw","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":5093,"completion_tokens":1,"total_tokens":5094,"prompt_tokens_details":{"cached_tokens":4864,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] -- non-streaming -- { - "id": "0d736322-1f3a-4748-9587-2a88dc42c664", + "id": "chatcmpl-C0qiv02TIWzrzJWAQq3GGxbX8ePiL", "object": "chat.completion", "created": 1754318705, "model": "gpt-4.1-2025-04-14", @@ -120,7 +120,7 @@ data: [DONE] -- non-streaming/tool-call -- { - "id": "496bca0d-c6bd-4607-89a5-169b4affa4f2", + "id": "chatcmpl-C0qiwr0OioIFZvyn4wcsrMy5cvncY", "object": "chat.completion", "created": 1754318706, "model": "gpt-4.1-2025-04-14", From 2faad0f1cac3a2043c00b60672ae01ad032902fe Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 5 Aug 2025 14:21:18 +0200 Subject: [PATCH 49/61] validate response IDs Signed-off-by: Danny Kopping --- aibridged/bridge.go | 10 +-- ...dge_test.go => bridge_integration_test.go} | 88 +++++++++++++++++-- 2 files changed, 84 insertions(+), 14 deletions(-) rename aibridged/{bridge_test.go => bridge_integration_test.go} (89%) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index cc4f9374af7c3..dc435b7e55fe4 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -274,7 +274,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ SessionId: sessionID, MsgId: chunk.ID, - Model: string(in.Model), + Model: in.Model, Input: toolCall.Arguments, Tool: toolCall.Name, }) @@ -364,7 +364,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ SessionId: sessionID, MsgId: acc.ID, - Model: string(in.Model), + Model: in.Model, Input: tc.Arguments, Tool: tc.Name, // TODO: sanitize tool name. Injected: true, @@ -460,7 +460,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ SessionId: sessionID, MsgId: completion.ID, - Model: string(completion.Model), + Model: completion.Model, InputTokens: cumulativeUsage.PromptTokens, OutputTokens: cumulativeUsage.CompletionTokens, Other: map[string]int64{ @@ -485,7 +485,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ SessionId: sessionID, MsgId: completion.ID, - Model: string(in.Model), + Model: in.Model, Input: toolCall.Function.Arguments, Tool: toolCall.Function.Name, }) @@ -519,7 +519,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ SessionId: sessionID, MsgId: completion.ID, - Model: string(in.Model), + Model: in.Model, Input: tc.Function.Arguments, Tool: fn, // TODO: sanitize tool name. Injected: true, diff --git a/aibridged/bridge_test.go b/aibridged/bridge_integration_test.go similarity index 89% rename from aibridged/bridge_test.go rename to aibridged/bridge_integration_test.go index 4d81716d443cb..72db7854bd708 100644 --- a/aibridged/bridge_test.go +++ b/aibridged/bridge_integration_test.go @@ -16,10 +16,12 @@ import ( "testing" "golang.org/x/tools/txtar" + "golang.org/x/xerrors" "storj.io/drpc" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/packages/ssestream" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/sjson" @@ -409,9 +411,9 @@ func TestOpenAIChatCompletions(t *testing.T) { { streaming: true, }, - // { - // streaming: false, - // }, + { + streaming: false, + }, } for _, tc := range cases { @@ -492,7 +494,6 @@ func TestOpenAIChatCompletions(t *testing.T) { // We must ALWAYS have 2 calls to the bridge. require.Eventually(t, func() bool { return mockSrv.callCount.Load() == 2 }, testutil.WaitLong, testutil.IntervalFast) - // TODO: this is a bit flimsy since this API won't be in beta forever. var content *openai.ChatCompletionChoice if tc.streaming { // Parse the response stream. @@ -512,7 +513,6 @@ func TestOpenAIChatCompletions(t *testing.T) { out, err := io.ReadAll(resp.Body) require.NoError(t, err) - // TODO: this is a bit flimsy since this API won't be in beta forever. var message openai.ChatCompletion require.NoError(t, json.Unmarshal(out, &message)) require.NotNil(t, message) @@ -534,10 +534,11 @@ func TestSimple(t *testing.T) { sessionToken := getSessionToken(t, client) testCases := []struct { - name string - fixture []byte - configureFunc func(string, proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) - createRequest func(*testing.T, string, []byte) *http.Request + name string + fixture []byte + configureFunc func(string, proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) + getResponseIDFunc func(bool, *http.Response) (string, error) + createRequest func(*testing.T, string, []byte) *http.Request }{ { name: "anthropic", @@ -554,6 +555,36 @@ func TestSimple(t *testing.T) { return client, true }, nil) }, + getResponseIDFunc: func(streaming bool, resp *http.Response) (string, error) { + if streaming { + decoder := ssestream.NewDecoder(resp) + // TODO: this is a bit flimsy since this API won't be in beta forever. + stream := ssestream.NewStream[anthropic.BetaRawMessageStreamEventUnion](decoder, nil) + var message anthropic.BetaMessage + for stream.Next() { + event := stream.Current() + if err := message.Accumulate(event); err != nil { + return "", xerrors.Errorf("accumulate event: %w", err) + } + } + if stream.Err() != nil { + return "", xerrors.Errorf("stream error: %w", stream.Err()) + } + return message.ID, nil + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", xerrors.Errorf("read body: %w", err) + } + + // TODO: this is a bit flimsy since this API won't be in beta forever. + var message anthropic.BetaMessage + if err := json.Unmarshal(body, &message); err != nil { + return "", xerrors.Errorf("unmarshal response: %w", err) + } + return message.ID, nil + }, createRequest: createAnthropicMessagesReq, }, { @@ -571,6 +602,34 @@ func TestSimple(t *testing.T) { return client, true }, nil) }, + getResponseIDFunc: func(streaming bool, resp *http.Response) (string, error) { + if streaming { + // Parse the response stream. + decoder := oai_ssestream.NewDecoder(resp) + stream := oai_ssestream.NewStream[openai.ChatCompletionChunk](decoder, nil) + var message openai.ChatCompletionAccumulator + for stream.Next() { + chunk := stream.Current() + message.AddChunk(chunk) + } + if stream.Err() != nil { + return "", xerrors.Errorf("stream error: %w", stream.Err()) + } + return message.ID, nil + } + + // Parse & unmarshal the response. + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", xerrors.Errorf("read body: %w", err) + } + + var message openai.ChatCompletion + if err := json.Unmarshal(body, &message); err != nil { + return "", xerrors.Errorf("unmarshal response: %w", err) + } + return message.ID, nil + }, createRequest: createOpenAIChatCompletionsReq, }, } @@ -630,9 +689,20 @@ func TestSimple(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, bodyBytes, "should have received response body") + // Reset the body after being read. + resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + // Then: I expect the prompt to have been tracked. require.NotEmpty(t, coderdClient.userPrompts, "no prompts tracked") assert.Equal(t, "how many angels can dance on the head of a pin", coderdClient.userPrompts[0].Prompt) + + // Validate that responses have their IDs overridden with a session ID rather than the original ID from the upstream provider. + // The reason for this is that Bridge may make multiple upstream requests (i.e. to invoke injected tools), and clients will not be expecting + // multiple messages in response to a single request. + // TODO: validate that expected upstream message ID is captured alongside returned ID in token usage. + id, err := tc.getResponseIDFunc(sc.streaming, resp) + require.NoError(t, err, "failed to retrieve response ID") + require.Nil(t, uuid.Validate(id), "id is not a UUID") }) } }) From 645b6df560a9414a33ccaed3159af63c6859cea7 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 6 Aug 2025 14:24:32 +0200 Subject: [PATCH 50/61] abstract injected tool test, check input Signed-off-by: Danny Kopping --- aibridged/bridge.go | 351 +++++------- aibridged/bridge_integration_test.go | 507 +++++++++--------- .../anthropic/single_injected_tool.txtar | 94 +++- .../openai/single_injected_tool.txtar | 290 +++++++--- aibridged/mcp.go | 6 +- 5 files changed, 671 insertions(+), 577 deletions(-) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index dc435b7e55fe4..5ea077fc11571 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -143,13 +143,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { defer cancel() r = r.WithContext(ctx) // Rewire context for SSE cancellation. - coderdClient, ok := b.clientFn() - if !ok { - // TODO: log issue. - http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) - return - } - body, err := io.ReadAll(r.Body) if err != nil { if isConnectionError(err) { @@ -170,14 +163,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { prompt, err := in.LastUserPrompt() // TODO: error handling. if prompt != nil { - if _, err = coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ - SessionId: sessionID, - MsgId: "", // Not yet known. We could move this after the response, but probably better to track prospectively. - Model: in.Model, - Prompt: *prompt, - }); err != nil { - b.logger.Error(r.Context(), "failed to track user prompt", slog.Error(err)) - } + b.trackUserPrompt(ctx, sessionID, "", in.Model, *prompt) } // Prepend assistant message. @@ -219,6 +205,8 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { opts = append(opts, oai_option.WithBaseURL(baseURL)) } + opts = append(opts, oai_option.WithMiddleware(LoggingMiddleware)) + client := openai.NewClient(opts...) req := in.ChatCompletionNewParams @@ -261,7 +249,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { chunk := stream.Current() acc.AddChunk(chunk) - fmt.Printf("[in]: %s\n", chunk.RawJSON()) + // fmt.Printf("[in]: %s\n", chunk.RawJSON()) shouldRelayChunk := true if toolCall, ok := acc.JustFinishedToolCall(); ok { @@ -271,16 +259,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // Don't relay this chunk back; we'll handle it transparently. shouldRelayChunk = false } else { - _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: chunk.ID, - Model: in.Model, - Input: toolCall.Arguments, - Tool: toolCall.Name, - }) - if err != nil { - b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) - } + b.trackToolUsage(ctx, sessionID, chunk.ID, in.Model, toolCall.Name, toolCall.Arguments, false) } } @@ -307,23 +286,14 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { // If the usage information is set, track it. // The API will send usage information when the response terminates, which will happen if a tool call is invoked. - if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ - SessionId: sessionID, - MsgId: acc.ID, - Model: string(acc.Model), - InputTokens: cumulativeUsage.PromptTokens, - OutputTokens: cumulativeUsage.CompletionTokens, - Other: map[string]int64{ - "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, - "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, - "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, - "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, - "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, - "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, - }, - }); err != nil { - b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) - } + b.trackTokenUsage(ctx, sessionID, acc.ID, string(acc.Model), cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, map[string]int64{ + "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, + "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, + "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, + "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, + "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, + "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, + }) if err := stream.Err(); err != nil { b.logger.Error(ctx, "server stream error", slog.Error(err)) @@ -361,17 +331,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { appendedPrevMsg = true } - _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: acc.ID, - Model: in.Model, - Input: tc.Arguments, - Tool: tc.Name, // TODO: sanitize tool name. - Injected: true, - }) - if err != nil { - b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) - } + b.trackToolUsage(ctx, sessionID, acc.ID, in.Model, tc.Name, tc.Arguments, true) var args map[string]any if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil { @@ -457,23 +417,14 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { cumulativeUsage = sumUsage(cumulativeUsage, completion.Usage) // Track token usage - if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ - SessionId: sessionID, - MsgId: completion.ID, - Model: completion.Model, - InputTokens: cumulativeUsage.PromptTokens, - OutputTokens: cumulativeUsage.CompletionTokens, - Other: map[string]int64{ - "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, - "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, - "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, - "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, - "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, - "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, - }, - }); err != nil { - b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) - } + b.trackTokenUsage(ctx, sessionID, completion.ID, completion.Model, cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, map[string]int64{ + "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, + "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, + "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, + "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, + "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, + "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, + }) // Check if we have tool calls to process var pendingToolCalls []openai.ChatCompletionMessageToolCall @@ -482,16 +433,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { if b.isInjectedTool(toolCall.Function.Name) { pendingToolCalls = append(pendingToolCalls, toolCall) } else { - _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: completion.ID, - Model: in.Model, - Input: toolCall.Function.Arguments, - Tool: toolCall.Function.Name, - }) - if err != nil { - b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) - } + b.trackToolUsage(ctx, sessionID, completion.ID, in.Model, toolCall.Function.Name, toolCall.Function.Arguments, false) } } } @@ -516,17 +458,7 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { appendedPrevMsg = true } - _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: completion.ID, - Model: in.Model, - Input: tc.Function.Arguments, - Tool: fn, // TODO: sanitize tool name. - Injected: true, - }) - if err != nil { - b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) - } + b.trackToolUsage(ctx, sessionID, completion.ID, in.Model, fn, tc.Function.Arguments, true) var ( args map[string]string @@ -636,13 +568,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { defer cancel() r = r.WithContext(ctx) // Rewire context for SSE cancellation. - coderdClient, ok := b.clientFn() - if !ok { - // TODO: log issue. - http.Error(w, "could not acquire coderd client", http.StatusInternalServerError) - return - } - useBeta := r.URL.Query().Get("beta") == "true" if !useBeta { b.logger.Warn(r.Context(), "non-beta API requested, using beta instead", slog.F("url", r.URL.String())) @@ -729,16 +654,9 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // Find the most recent user message and track the prompt. if !isSmallFastModel { - prompt, err := in.LastUserPrompt() // TODO: error handling. + prompt, _ := in.LastUserPrompt() // TODO: error handling. if prompt != nil { - if _, err = coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ - SessionId: sessionID, - MsgId: "", // Not yet known. We could move this after the response, but probably better to track prospectively. - Prompt: *prompt, - Model: string(in.Model), - }); err != nil { - b.logger.Error(r.Context(), "failed to track user prompt", slog.Error(err)) - } + b.trackUserPrompt(ctx, sessionID, "", string(in.Model), *prompt) } } @@ -789,22 +707,13 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { return } - if _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ - SessionId: sessionID, - MsgId: resp.ID, - Model: string(resp.Model), - InputTokens: resp.Usage.InputTokens, - OutputTokens: resp.Usage.OutputTokens, - Other: map[string]int64{ - "web_search_requests": resp.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": resp.Usage.CacheCreationInputTokens, - "cache_read_input": resp.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": resp.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": resp.Usage.CacheCreation.Ephemeral5mInputTokens, - }, - }); err != nil { - b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) - } + b.trackTokenUsage(ctx, sessionID, resp.ID, string(resp.Model), resp.Usage.InputTokens, resp.Usage.OutputTokens, map[string]int64{ + "web_search_requests": resp.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": resp.Usage.CacheCreationInputTokens, + "cache_read_input": resp.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": resp.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": resp.Usage.CacheCreation.Ephemeral5mInputTokens, + }) // Handle tool calls for non-streaming. var pendingToolCalls []anthropic.BetaToolUseBlock @@ -820,20 +729,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } // If tool is not injected, track it since the client will be handling it. - if serialized, err := json.Marshal(toolUse.Input); err == nil { - _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: resp.ID, - Model: string(resp.Model), - Input: string(serialized), - Tool: toolUse.Name, - }) - if err != nil { - b.logger.Error(ctx, "failed to track tool usage", slog.Error(err)) - } - } else { - b.logger.Warn(ctx, "failed to marshal args for tool usage", slog.Error(err)) - } + b.trackToolUsage(ctx, sessionID, resp.ID, string(resp.Model), toolUse.Name, toolUse.Input, false) } // If no injected tool calls, we're done. @@ -879,17 +775,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { continue } - _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: resp.ID, - Model: string(resp.Model), - Input: string(serialized), - Tool: tc.Name, // TODO: sanitize tool name. - Injected: true, - }) - if err != nil { - b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) - } + b.trackToolUsage(ctx, sessionID, resp.ID, string(resp.Model), tc.Name, args, true) res, err := tool.Call(ctx, args) if err != nil { @@ -1049,22 +935,13 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // Anthropic's docs only mention usage in message_delta events, but it's also present in message_start. // See https://docs.anthropic.com/en/docs/build-with-claude/streaming#event-types. start := event.AsMessageStart() - if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ - SessionId: sessionID, - MsgId: message.ID, - Model: string(message.Model), - InputTokens: start.Message.Usage.InputTokens, - OutputTokens: start.Message.Usage.OutputTokens, - Other: map[string]int64{ - "web_search_requests": start.Message.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": start.Message.Usage.CacheCreationInputTokens, - "cache_read_input": start.Message.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, - }, - }); err != nil { - b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) - } + b.trackTokenUsage(streamCtx, sessionID, message.ID, string(message.Model), start.Message.Usage.InputTokens, start.Message.Usage.OutputTokens, map[string]int64{ + "web_search_requests": start.Message.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": start.Message.Usage.CacheCreationInputTokens, + "cache_read_input": start.Message.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, + }) if !isFirst { // Don't send message_start unless first message! @@ -1074,22 +951,13 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): delta := event.AsMessageDelta() - if _, err = coderdClient.TrackTokenUsage(streamCtx, &proto.TrackTokenUsageRequest{ - SessionId: sessionID, - MsgId: message.ID, - Model: string(message.Model), - InputTokens: delta.Usage.InputTokens, - OutputTokens: delta.Usage.OutputTokens, - Other: map[string]int64{ - "web_search_requests": delta.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": delta.Usage.CacheCreationInputTokens, - "cache_read_input": delta.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, - }, - }); err != nil { - b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) - } + b.trackTokenUsage(streamCtx, sessionID, message.ID, string(message.Model), delta.Usage.InputTokens, delta.Usage.OutputTokens, map[string]int64{ + "web_search_requests": delta.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": delta.Usage.CacheCreationInputTokens, + "cache_read_input": delta.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, + }) // Don't relay message_delta events which indicate injected tool use. if len(pendingToolCalls) > 0 && b.isInjectedTool(lastToolName) { @@ -1140,23 +1008,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { continue } - fmt.Printf("[event] %s\n[tool(%q)] %s %+v\n\n", event.RawJSON(), id, name, input) - - if serialized, err := json.Marshal(input); err == nil { - _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: message.ID, - Model: string(message.Model), - Input: string(serialized), - Tool: tool.Name, // TODO: sanitize tool name. - Injected: true, - }) - if err != nil { - b.logger.Error(ctx, "failed to track injected tool usage", slog.Error(err)) - } - } else { - b.logger.Warn(ctx, "failed to marshal args for tool usage", slog.Error(err)) - } + b.trackToolUsage(streamCtx, sessionID, message.ID, string(message.Model), tool.Name, input, true) res, err := b.tools[tool.ID].Call(streamCtx, input) if err != nil { @@ -1263,10 +1115,6 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { // If no content was processed, still add a tool_result if !hasValidResult { - // messages.Messages = append(messages.Messages, - // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, "Error: no valid tool result content", true)), - //) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ OfText: &anthropic.BetaTextBlockParam{ Text: "Error: no valid tool result content", @@ -1292,20 +1140,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { continue } - if serialized, err := json.Marshal(variant.Input); err == nil { - _, err = coderdClient.TrackToolUsage(streamCtx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: message.ID, - Model: string(message.Model), - Input: string(serialized), - Tool: variant.Name, - }) - if err != nil { - b.logger.Error(ctx, "failed to track non-injected tool usage", slog.Error(err)) - } - } else { - b.logger.Warn(ctx, "failed to marshal args for tool usage", slog.Error(err)) - } + b.trackToolUsage(streamCtx, sessionID, message.ID, string(message.Model), variant.Name, variant.Input, false) } } } @@ -1357,6 +1192,90 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } } +func (b *Bridge) trackToolUsage(ctx context.Context, sessionID, msgID, model, toolName string, toolInput interface{}, injected bool) { + coderdClient, ok := b.clientFn() + if !ok { + b.logger.Error(ctx, "could not acquire coderd client for tool usage tracking") + return + } + + var input string + // TODO: unmarshal instead of marshal? i.e. persist maps rather than strings? + switch val := toolInput.(type) { + case string: + input = val + case []byte: + input = string(val) + default: + encoded, err := json.Marshal(toolInput) + if err == nil { + input = string(encoded) + } else { + b.logger.Error(ctx, "failed to marshal tool input", slog.Error(err), slog.F("injected", injected), slog.F("tool_name", toolName)) + input = fmt.Sprintf("%v", val) + } + } + + // For injected tools: strip MCP tool namespacing, if possible. + if injected { + _, tool, err := DecodeToolID(toolName) + // No recourse here; no point in logging - we'll see it in the database anyway. + if err == nil { + toolName = tool + } + } + + _, err := coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + SessionId: sessionID, + MsgId: msgID, + Model: model, + Input: input, + Tool: toolName, + Injected: injected, + }) + if err != nil { + b.logger.Error(ctx, "failed to track tool usage", slog.Error(err), slog.F("injected", injected)) + } +} + +func (b *Bridge) trackUserPrompt(ctx context.Context, sessionID, msgID, model, prompt string) { + coderdClient, ok := b.clientFn() + if !ok { + b.logger.Error(ctx, "could not acquire coderd client for user prompt tracking") + return + } + + _, err := coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ + SessionId: sessionID, + MsgId: msgID, + Model: model, + Prompt: prompt, + }) + if err != nil { + b.logger.Error(ctx, "failed to track user prompt", slog.Error(err)) + } +} + +func (b *Bridge) trackTokenUsage(ctx context.Context, sessionID, msgID, model string, inputTokens, outputTokens int64, other map[string]int64) { + coderdClient, ok := b.clientFn() + if !ok { + b.logger.Error(ctx, "could not acquire coderd client for token usage tracking") + return + } + + _, err := coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + SessionId: sessionID, + MsgId: msgID, + Model: model, + InputTokens: inputTokens, + OutputTokens: outputTokens, + Other: other, + }) + if err != nil { + b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) + } +} + func (b *Bridge) isInjectedTool(id string) bool { _, ok := b.tools[id] return ok diff --git a/aibridged/bridge_integration_test.go b/aibridged/bridge_integration_test.go index 72db7854bd708..57b80feb8429d 100644 --- a/aibridged/bridge_integration_test.go +++ b/aibridged/bridge_integration_test.go @@ -169,131 +169,6 @@ func TestAnthropicMessages(t *testing.T) { } }) - // TODO: fixture contains hardcoded tool name; this is flimsy since our naming convention may change for injected tools. - t.Run("single injected tool", func(t *testing.T) { - t.Parallel() - - cases := []struct { - streaming bool - }{ - { - streaming: true, - }, - { - streaming: false, - }, - } - - for _, tc := range cases { - t.Run(fmt.Sprintf("%s/streaming=%v", t.Name(), tc.streaming), func(t *testing.T) { - t.Parallel() - - arc := txtar.Parse(antSingleInjectedTool) - t.Logf("%s: %s", t.Name(), arc.Comment) - - files := filesMap(arc) - require.Len(t, files, 5) - require.Contains(t, files, fixtureRequest) - require.Contains(t, files, fixtureStreamingResponse) - require.Contains(t, files, fixtureNonStreamingResponse) - require.Contains(t, files, fixtureStreamingToolResponse) - require.Contains(t, files, fixtureNonStreamingToolResponse) - - reqBody := files[fixtureRequest] - - // Add the stream param to the request. - newBody, err := sjson.SetBytes(reqBody, "stream", tc.streaming) - require.NoError(t, err) - reqBody = newBody - - ctx := testutil.Context(t, testutil.WaitLong) - // Conditionally return fixtures based on request count. - // First request: halts with tool call instruction. - // Second request: tool call invocation. - mockSrv := newMockServer(ctx, t, files, func(reqCount uint32, resp []byte) []byte { - if reqCount == 1 { - return resp - } - - if reqCount > 2 { - t.Fatalf("did not expect more than 2 calls; received %d", reqCount) - } - - if !tc.streaming { - return files[fixtureNonStreamingToolResponse] - } - return files[fixtureStreamingToolResponse] - }) - t.Cleanup(mockSrv.Close) - - coderdClient := &fakeBridgeDaemonClient{} - logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) - - // Setup Coder MCP integration. - mcpSrv := httptest.NewServer(createMockMCPSrv(t)) - mcpBridge, err := aibridged.NewMCPToolBridge("coder", mcpSrv.URL, map[string]string{}, logger) - require.NoError(t, err) - - // Initialize MCP client, fetch tools, and inject into bridge. - require.NoError(t, mcpBridge.Init(testutil.Context(t, testutil.WaitShort))) - tools := mcpBridge.ListTools() - require.NotEmpty(t, tools) - - b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - Anthropic: codersdk.AIBridgeAnthropicConfig{ - BaseURL: serpent.String(mockSrv.URL), - Key: serpent.String(sessionToken), - }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return coderdClient, true - }, tools) - require.NoError(t, err) - - // Invoke request to mocked API via aibridge. - bridgeSrv := httptest.NewServer(b.Handler()) - req := createAnthropicMessagesReq(t, bridgeSrv.URL, reqBody) - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - defer resp.Body.Close() - - // We must ALWAYS have 2 calls to the bridge. - require.Eventually(t, func() bool { return mockSrv.callCount.Load() == 2 }, testutil.WaitLong, testutil.IntervalFast) - - // TODO: this is a bit flimsy since this API won't be in beta forever. - var content *anthropic.BetaContentBlockUnion - if tc.streaming { - // Parse the response stream. - decoder := ssestream.NewDecoder(resp) - stream := ssestream.NewStream[anthropic.BetaRawMessageStreamEventUnion](decoder, nil) - var message anthropic.BetaMessage - for stream.Next() { - event := stream.Current() - require.NoError(t, message.Accumulate(event)) - } - require.NoError(t, stream.Err()) - require.Len(t, message.Content, 2) - content = &message.Content[1] - } else { - // Parse & unmarshal the response. - out, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - // TODO: this is a bit flimsy since this API won't be in beta forever. - var message anthropic.BetaMessage - require.NoError(t, json.Unmarshal(out, &message)) - require.NotNil(t, message) - require.Len(t, message.Content, 1) - content = &message.Content[0] - } - - require.NotNil(t, content) - require.Equal(t, "admin", content.Text) - }) - } - }) } func TestOpenAIChatCompletions(t *testing.T) { @@ -401,130 +276,6 @@ func TestOpenAIChatCompletions(t *testing.T) { } }) - // TODO: fixture contains hardcoded tool name; this is flimsy since our naming convention may change for injected tools. - t.Run("single injected tool", func(t *testing.T) { - t.Parallel() - - cases := []struct { - streaming bool - }{ - { - streaming: true, - }, - { - streaming: false, - }, - } - - for _, tc := range cases { - t.Run(fmt.Sprintf("%s/streaming=%v", t.Name(), tc.streaming), func(t *testing.T) { - t.Parallel() - - arc := txtar.Parse(oaiSingleInjectedTool) - t.Logf("%s: %s", t.Name(), arc.Comment) - - files := filesMap(arc) - require.Len(t, files, 5) - require.Contains(t, files, fixtureRequest) - require.Contains(t, files, fixtureStreamingResponse) - require.Contains(t, files, fixtureNonStreamingResponse) - require.Contains(t, files, fixtureStreamingToolResponse) - require.Contains(t, files, fixtureNonStreamingToolResponse) - - reqBody := files[fixtureRequest] - - // Add the stream param to the request. - newBody, err := sjson.SetBytes(reqBody, "stream", tc.streaming) - require.NoError(t, err) - reqBody = newBody - - ctx := testutil.Context(t, testutil.WaitLong) - // Conditionally return fixtures based on request count. - // First request: halts with tool call instruction. - // Second request: tool call invocation. - mockSrv := newMockServer(ctx, t, files, func(reqCount uint32, resp []byte) []byte { - if reqCount == 1 { - return resp - } - - if reqCount > 2 { - t.Fatalf("did not expect more than 2 calls; received %d", reqCount) - } - - if !tc.streaming { - return files[fixtureNonStreamingToolResponse] - } - return files[fixtureStreamingToolResponse] - }) - t.Cleanup(mockSrv.Close) - - coderdClient := &fakeBridgeDaemonClient{} - logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) - - // Setup Coder MCP integration. - mcpSrv := httptest.NewServer(createMockMCPSrv(t)) - mcpBridge, err := aibridged.NewMCPToolBridge("coder", mcpSrv.URL, map[string]string{}, logger) - require.NoError(t, err) - - // Initialize MCP client, fetch tools, and inject into bridge. - require.NoError(t, mcpBridge.Init(testutil.Context(t, testutil.WaitShort))) - tools := mcpBridge.ListTools() - require.NotEmpty(t, tools) - - b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - OpenAI: codersdk.AIBridgeOpenAIConfig{ - BaseURL: serpent.String(mockSrv.URL), - Key: serpent.String(sessionToken), - }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return coderdClient, true - }, tools) - require.NoError(t, err) - - // Invoke request to mocked API via aibridge. - bridgeSrv := httptest.NewServer(b.Handler()) - req := createOpenAIChatCompletionsReq(t, bridgeSrv.URL, reqBody) - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - defer resp.Body.Close() - - // We must ALWAYS have 2 calls to the bridge. - require.Eventually(t, func() bool { return mockSrv.callCount.Load() == 2 }, testutil.WaitLong, testutil.IntervalFast) - - var content *openai.ChatCompletionChoice - if tc.streaming { - // Parse the response stream. - decoder := oai_ssestream.NewDecoder(resp) - stream := oai_ssestream.NewStream[openai.ChatCompletionChunk](decoder, nil) - var message openai.ChatCompletionAccumulator - for stream.Next() { - chunk := stream.Current() - message.AddChunk(chunk) - } - - require.NoError(t, stream.Err()) - require.Len(t, message.Choices, 1) - content = &message.Choices[0] - } else { - // Parse & unmarshal the response. - out, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - var message openai.ChatCompletion - require.NoError(t, json.Unmarshal(out, &message)) - require.NotNil(t, message) - require.Len(t, message.Choices, 1) - content = &message.Choices[0] - } - - require.NotNil(t, content) - require.Contains(t, content.Message.Content, "admin") - }) - } - }) } func TestSimple(t *testing.T) { @@ -709,6 +460,255 @@ func TestSimple(t *testing.T) { } } +// setupMCPToolsForTest creates a mock MCP server, initializes the MCP bridge, and returns the tools +func setupMCPToolsForTest(t *testing.T) []*aibridged.MCPTool { + t.Helper() + + // Setup Coder MCP integration + mcpSrv := httptest.NewServer(createMockMCPSrv(t)) + t.Cleanup(mcpSrv.Close) + + logger := testutil.Logger(t) + mcpBridge, err := aibridged.NewMCPToolBridge("coder", mcpSrv.URL, map[string]string{}, logger) + require.NoError(t, err) + + // Initialize MCP client, fetch tools, and inject into bridge + require.NoError(t, mcpBridge.Init(testutil.Context(t, testutil.WaitShort))) + tools := mcpBridge.ListTools() + require.NotEmpty(t, tools) + + return tools +} + +// TestInjectedTool is an abstracted test function for "single injected tool" scenarios +// that works with both Anthropic and OpenAI providers +func TestInjectedTool(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + sessionToken := getSessionToken(t, client) + + testCases := []struct { + name string + fixture []byte + configureFunc func(string, proto.DRPCAIBridgeDaemonClient, []*aibridged.MCPTool) (*aibridged.Bridge, error) + getResponseContentFunc func(bool, *http.Response) (string, error) + createRequest func(*testing.T, string, []byte) *http.Request + }{ + { + name: "anthropic", + fixture: antSingleInjectedTool, + configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools []*aibridged.MCPTool) (*aibridged.Bridge, error) { + logger := testutil.Logger(t) + return aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + Anthropic: codersdk.AIBridgeAnthropicConfig{ + BaseURL: serpent.String(addr), + Key: serpent.String(sessionToken), + }, + }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return client, true + }, tools) + }, + getResponseContentFunc: func(streaming bool, resp *http.Response) (string, error) { + // TODO: this is a bit flimsy since this API won't be in beta forever. + var content *anthropic.BetaContentBlockUnion + if streaming { + // Parse the response stream. + decoder := ssestream.NewDecoder(resp) + stream := ssestream.NewStream[anthropic.BetaRawMessageStreamEventUnion](decoder, nil) + var message anthropic.BetaMessage + for stream.Next() { + event := stream.Current() + if err := message.Accumulate(event); err != nil { + return "", xerrors.Errorf("accumulate event: %w", err) + } + } + if stream.Err() != nil { + return "", xerrors.Errorf("stream error: %w", stream.Err()) + } + if len(message.Content) < 2 { + return "", xerrors.Errorf("expected at least 2 content blocks, got %d", len(message.Content)) + } + content = &message.Content[1] + } else { + // Parse & unmarshal the response. + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", xerrors.Errorf("read body: %w", err) + } + + var message anthropic.BetaMessage + if err := json.Unmarshal(body, &message); err != nil { + return "", xerrors.Errorf("unmarshal response: %w", err) + } + if len(message.Content) == 0 { + return "", xerrors.Errorf("no content blocks in response") + } + content = &message.Content[0] + } + + if content == nil { + return "", xerrors.Errorf("content is nil") + } + return content.Text, nil + }, + createRequest: createAnthropicMessagesReq, + }, + { + name: "openai", + fixture: oaiSingleInjectedTool, + configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools []*aibridged.MCPTool) (*aibridged.Bridge, error) { + logger := testutil.Logger(t) + return aibridged.NewBridge(codersdk.AIBridgeConfig{ + Daemons: 1, + OpenAI: codersdk.AIBridgeOpenAIConfig{ + BaseURL: serpent.String(addr), + Key: serpent.String(sessionToken), + }, + }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { + return client, true + }, tools) + }, + getResponseContentFunc: func(streaming bool, resp *http.Response) (string, error) { + var content *openai.ChatCompletionChoice + if streaming { + // Parse the response stream. + decoder := oai_ssestream.NewDecoder(resp) + stream := oai_ssestream.NewStream[openai.ChatCompletionChunk](decoder, nil) + var message openai.ChatCompletionAccumulator + for stream.Next() { + chunk := stream.Current() + message.AddChunk(chunk) + } + + if stream.Err() != nil { + return "", xerrors.Errorf("stream error: %w", stream.Err()) + } + if len(message.Choices) == 0 { + return "", xerrors.Errorf("no choices in response") + } + content = &message.Choices[0] + } else { + // Parse & unmarshal the response. + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", xerrors.Errorf("read body: %w", err) + } + + var message openai.ChatCompletion + if err := json.Unmarshal(body, &message); err != nil { + return "", xerrors.Errorf("unmarshal response: %w", err) + } + if len(message.Choices) == 0 { + return "", xerrors.Errorf("no choices in response") + } + content = &message.Choices[0] + } + + if content == nil { + return "", xerrors.Errorf("content is nil") + } + return content.Message.Content, nil + }, + createRequest: createOpenAIChatCompletionsReq, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + streamingCases := []struct { + streaming bool + }{ + {streaming: true}, + {streaming: false}, + } + + for _, sc := range streamingCases { + t.Run(fmt.Sprintf("streaming=%v", sc.streaming), func(t *testing.T) { + t.Parallel() + + arc := txtar.Parse(tc.fixture) + t.Logf("%s: %s", t.Name(), arc.Comment) + + files := filesMap(arc) + require.Len(t, files, 5) + require.Contains(t, files, fixtureRequest) + require.Contains(t, files, fixtureStreamingResponse) + require.Contains(t, files, fixtureNonStreamingResponse) + require.Contains(t, files, fixtureStreamingToolResponse) + require.Contains(t, files, fixtureNonStreamingToolResponse) + + reqBody := files[fixtureRequest] + + // Add the stream param to the request. + newBody, err := sjson.SetBytes(reqBody, "stream", sc.streaming) + require.NoError(t, err) + reqBody = newBody + + ctx := testutil.Context(t, testutil.WaitLong) + + // Setup mock server with response mutator for multi-turn interaction. + mockSrv := newMockServer(ctx, t, files, func(reqCount uint32, resp []byte) []byte { + if reqCount == 1 { + return resp // First request gets the normal response (with tool call) + } + + if reqCount > 2 { + // This should not happen in single injected tool tests + return resp + } + + // Second request gets the tool response + if sc.streaming { + return files[fixtureStreamingToolResponse] + } + return files[fixtureNonStreamingToolResponse] + }) + t.Cleanup(mockSrv.Close) + + coderdClient := &fakeBridgeDaemonClient{} + + // Setup MCP tools. + tools := setupMCPToolsForTest(t) + + // Configure the bridge with injected tools. + b, err := tc.configureFunc(mockSrv.URL, coderdClient, tools) + require.NoError(t, err) + + // Invoke request to mocked API via aibridge. + bridgeSrv := httptest.NewServer(b.Handler()) + t.Cleanup(bridgeSrv.Close) + + req := tc.createRequest(t, bridgeSrv.URL, reqBody) + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() + + // We must ALWAYS have 2 calls to the bridge for injected tool tests + require.Eventually(t, func() bool { + return mockSrv.callCount.Load() == 2 + }, testutil.WaitLong, testutil.IntervalFast) + + // Ensure expected tool was invoked with expected input. + require.Len(t, coderdClient.toolUsages, 1) + require.Equal(t, mockToolName, coderdClient.toolUsages[0].Tool) + require.EqualValues(t, `{"owner":"admin"}`, coderdClient.toolUsages[0].Input) + + // Ensure tool returned expected value. + answer, err := tc.getResponseContentFunc(sc.streaming, resp) + require.NoError(t, err) + require.Contains(t, answer, "dd711d5c-83c6-4c08-a0af-b73055906e8c") // The ID of the workspace to be returned. + }) + } + }) + } +} + func calculateTotalOutputTokens(in []*proto.TrackTokenUsageRequest) int64 { var total int64 for _, el := range in { @@ -881,6 +881,8 @@ func (f *fakeBridgeDaemonClient) TrackToolUsage(ctx context.Context, in *proto.T return &proto.TrackToolUsageResponse{}, nil } +const mockToolName = "coder_list_workspaces" + func createMockMCPSrv(t *testing.T) http.Handler { t.Helper() @@ -890,12 +892,9 @@ func createMockMCPSrv(t *testing.T) http.Handler { server.WithToolCapabilities(true), ) - // Add tool - tool := mcp.NewTool("coder_get_authenticated_user", - mcp.WithDescription("Mock of the coder_get_authenticated_user tool"), + tool := mcp.NewTool(mockToolName, + mcp.WithDescription(fmt.Sprintf("Mock of the %s tool", mockToolName)), ) - - // Add tool handler s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return mcp.NewToolResultText("mock"), nil }) diff --git a/aibridged/fixtures/anthropic/single_injected_tool.txtar b/aibridged/fixtures/anthropic/single_injected_tool.txtar index 4c29b7c7f9a3c..33840730e4f3c 100644 --- a/aibridged/fixtures/anthropic/single_injected_tool.txtar +++ b/aibridged/fixtures/anthropic/single_injected_tool.txtar @@ -7,41 +7,53 @@ Coder MCP tools automatically injected. "messages": [ { "role": "user", - "content": "what is my coder username" + "content": "list coder workspace IDs for admin" } ] } -- streaming -- event: message_start -data: {"type":"message_start","message":{"id":"msg_01FHxVHRpKWz6dFfm9s1jkNY","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":385,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5,"service_tier":"standard"}} } +data: {"type":"message_start","message":{"id":"msg_01JWGa2JHsKBHL28Cjr2dvPK","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7545,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5,"service_tier":"standard"}} } event: content_block_start -data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } event: ping data: {"type": "ping"} event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll check your"} } +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll list the work"} } event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Coder username for you."} } +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"spaces for the admin user to get their"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" workspace IDs."} } event: content_block_stop -data: {"type":"content_block_stop","index":0 } +data: {"type":"content_block_stop","index":0 } event: content_block_start -data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_018gpsLYxqC9D7v4nuJL5Qvy","name":"__mcp__coder_coder_get_authenticated_user","input":{}} } +data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01TSQLR6R6wBUqoxGPjQKDAj","name":"__mcp__coder_coder_list_workspaces","input":{}} } + +event: content_block_delta +data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} } event: content_block_delta -data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} } +data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"owner\""} } + +event: content_block_delta +data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":": \"ad"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"min\"}"} } event: content_block_stop -data: {"type":"content_block_stop","index":1 } +data: {"type":"content_block_stop","index":1 } event: message_delta -data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":59} } +data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":75}} event: message_stop data: {"type":"message_stop" } @@ -49,52 +61,78 @@ data: {"type":"message_stop" } -- streaming/tool-call -- event: message_start -data: {"type":"message_start","message":{"id":"msg_01Rsww53iPc1nW2uEBrMkxH6","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1162,"cache_read_input_tokens":35876,"output_tokens":1,"service_tier":"standard"}} } +data: {"type":"message_start","message":{"id":"msg_01LZSVzMCLivzXrp6ZnTcmeG","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7763,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} } event: content_block_start -data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } event: ping data: {"type": "ping"} event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"admin"} } +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Here"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" are the workspace IDs for the admin user:\n\n**"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Admin's Workspaces:**\n- Workspace ID: `dd711d5c-83c"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"6-4c08-a0af-b73055906e8"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"c`\n - Name: `bob`\n - Template: `docker"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"`\n - Template ID: `b3a9d9b4-486a-4"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"f21-8884-d81d5dbdd837`"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\nThe admin user currently has 1 workspace named \"bob\" created from"} } + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the \"docker\" template."} } event: content_block_stop -data: {"type":"content_block_stop","index":0 } +data: {"type":"content_block_stop","index":0 } event: message_delta -data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":4} } +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":128} } event: message_stop -data: {"type":"message_stop" } +data: {"type":"message_stop" } -- non-streaming -- { - "id": "msg_018NqsW4eMw8tw47xBby5sV6", + "id": "msg_01FwkWU26guw9EwkL8zeacPL", "type": "message", "role": "assistant", "model": "claude-sonnet-4-20250514", "content": [ { "type": "text", - "text": "I'll check your current Coder username for you." + "text": "I'll list the workspaces for the admin user to get their workspace IDs." }, { "type": "tool_use", - "id": "toolu_01PNPxdk8GqP9uTpWH7C2wu1", - "name": "__mcp__coder_coder_get_authenticated_user", - "input": {} + "id": "toolu_01QjNz5b3HxAqAccTVnSMsKP", + "name": "__mcp__coder_coder_list_workspaces", + "input": { + "owner": "admin" + } } ], "stop_reason": "tool_use", "stop_sequence": null, "usage": { - "input_tokens": 385, + "input_tokens": 7545, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, - "output_tokens": 60, + "output_tokens": 75, "service_tier": "standard" } } @@ -102,23 +140,23 @@ data: {"type":"message_stop" } -- non-streaming/tool-call -- { - "id": "msg_01H7amafFFU8rkqPNNX7LoZ2", + "id": "msg_01Sr5BnPSwodTo8Df4XvUBg5", "type": "message", "role": "assistant", "model": "claude-sonnet-4-20250514", "content": [ { "type": "text", - "text": "admin" + "text": "Here are the Coder workspace IDs for the admin user:\n\n**Workspace ID:** `dd711d5c-83c6-4c08-a0af-b73055906e8c`\n- **Name:** bob\n- **Template:** docker\n- **Template ID:** b3a9d9b4-486a-4f21-8884-d81d5dbdd837\n- **Status:** Up to date (not outdated)\n\nThe admin user currently has 1 workspace named \"bob\" running on the \"docker\" template." } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { - "input_tokens": 7, - "cache_creation_input_tokens": 1175, + "input_tokens": 7763, + "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, - "output_tokens": 12, + "output_tokens": 129, "service_tier": "standard" } } diff --git a/aibridged/fixtures/openai/single_injected_tool.txtar b/aibridged/fixtures/openai/single_injected_tool.txtar index 2a1f96696ee26..b9670fc2323b3 100644 --- a/aibridged/fixtures/openai/single_injected_tool.txtar +++ b/aibridged/fixtures/openai/single_injected_tool.txtar @@ -6,130 +6,266 @@ Coder MCP tools automatically injected. "messages": [ { "role": "user", - "content": "what is my coder username" + "content": "list coder workspace IDs for admin" } ] } -- streaming -- -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ha7QSWuIrCLSg"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TxlRNztDyni152"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" am"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" am"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"d8rQaibDQpyL"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Qlbfp6UEp"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"68rb1Vo3ymBh"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" retrieve"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" call"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"i7c6mc6zJY"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" your"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Z9syl1x73E7"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" C"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" appropriate"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5wK"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"oder"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" tool"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"qxf0biXh4i"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" username"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UMXRLeWr9r7g"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" by"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" list"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PkO0yHjNu3"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" checking"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" all"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ktUBR7vT2FC"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" work"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xdNr1gCRJW"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" currently"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"spaces"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5z5luvhUz"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" authenticated"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"G6D7Ze3OlLR"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6BZ54FOiuA7"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6b0xOBQj2J"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_ksPDU5jx3nxrlTbSGXRNdMqN","type":"function","function":{"name":"__mcp__coder_coder_get_authenticated_user","arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" admin"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"X5gzNDQyO"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{}"}}]},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"oSONGErPa7g"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" display"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"EK9oGdN"} -data: {"id":"chatcmpl-C0qkbeDIPQzDWg0Xg3FCGJl3bCWTX","object":"chat.completion.chunk","created":1754318809,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":4860,"completion_tokens":35,"total_tokens":4895,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" their"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TPtBmjMIt"} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" IDs"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FONB73iSePd"} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"VMpWnam5jp"} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_0TxntkwDB66KH8z4RwNqeWrZ","type":"function","function":{"name":"__mcp__coder_coder_list_workspaces","arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kY"} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"n5"} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"owner"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"admin"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"1t"} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null,"obfuscation":"sDj"} + +data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[],"usage":{"prompt_tokens":4862,"completion_tokens":45,"total_tokens":4907,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"8sIWE1chOW"} data: [DONE] -- streaming/tool-call -- -data: {"id":"chatcmpl-C0qkci2O8GBHoOT04PukNschUStOw","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"DBu9uyty0Uhux"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"Here"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Pk0tDwr0wkd"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" are"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ACu9WW1Lsz4"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xrXWRUKKAZl"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LowCw"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" IDs"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RXNpYewll1k"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WnyxJrani1M"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"JrnDAJOLap4"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RNZIdDo4vj"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" admin"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nJ7O0qcsG"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"0k0UVPjnE2"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dtGIleZ8Nl9lU7"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wKNWu"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Name"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cmzvcWMEIp"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GsImQO12UCnPHY"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" bob"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AR4Jvn87StW"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WoNeyT7BKKjIS"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2Ou4DytumVPlyW"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PRWw3"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" ID"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"rrKKjluNdVET"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"v6NUOTV1Pd6piU"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" dd"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UuYGjaLT7OXO"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"711"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vLHjJVhbJgec"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"d"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2yDtuCir4L9eyS"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"5"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kyJOHcdfo1NMrP"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nuKRieC0bpf6O3"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"q29JHHRnNg1GYt"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"83"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"e0o7Zu6eKnter"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NCASF3SYR9GDQl"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"6"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"eG48V9XgxodtbB"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"CpP8ALTDfT0yBv"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"4"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"uQY85IhRAfuFl9"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wsdJSv3bN65S5a"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"08"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dq2JARx8gsgIm"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-a"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"4booyOM91IZdC"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"0"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wVJJDjNFBXO3OC"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"af"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"XFtDbXdnHdnF3"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-b"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"juymtEmZxo1Ez"} -data: {"id":"chatcmpl-C0qkci2O8GBHoOT04PukNschUStOw","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"admin"},"logprobs":null,"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"730"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"8pIOLoJZJAfe"} -data: {"id":"chatcmpl-C0qkci2O8GBHoOT04PukNschUStOw","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"559"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NPfQJmrtGPlY"} -data: {"id":"chatcmpl-C0qkci2O8GBHoOT04PukNschUStOw","object":"chat.completion.chunk","created":1754318810,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":5093,"completion_tokens":1,"total_tokens":5094,"prompt_tokens_details":{"cached_tokens":4864,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"06"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"jsqxOojcWTY3A"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"e"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cWYFwWie0ciIju"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"8"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ilVWzWQLUWQOMw"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ea99MtCCypPar2"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SDq7UD3LcH7"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"Let"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"S343Ji05lUgD"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" me"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TTCD9vPg98sO"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" know"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xcsP3lRI6f"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" if"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"bS0qh0vq73n3"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"pxUYdxCHoy8"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" need"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wjLDXO4uD8"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"B6ckyharjv"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" information"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xrN"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"aqv4RrWxJ"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" any"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hqdG5QSND4E"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"HvfgjMOXU6aG"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" these"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"yE0jSPMkD"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" work"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wWfGxJR2wt"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"spaces"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hOXndth8X"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"MReMwESHIpaDyo"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"EFeFvdS8m"} + +data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[],"usage":{"prompt_tokens":5049,"completion_tokens":60,"total_tokens":5109,"prompt_tokens_details":{"cached_tokens":4864,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"0JQt7Fw"} data: [DONE] -- non-streaming -- { - "id": "chatcmpl-C0qiv02TIWzrzJWAQq3GGxbX8ePiL", - "object": "chat.completion", - "created": 1754318705, - "model": "gpt-4.1-2025-04-14", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "I am about to retrieve your Coder username by querying your authenticated user information.", - "tool_calls": [ - { - "id": "call_O0miYTlPRzYjDf912pi3Ua1t", - "type": "function", - "function": { - "name": "__mcp__coder_coder_get_authenticated_user", - "arguments": "{}" - } - } - ], - "refusal": null, - "annotations": [] - }, - "logprobs": null, - "finish_reason": "tool_calls" - } - ], - "usage": { - "prompt_tokens": 4860, - "completion_tokens": 35, - "total_tokens": 4895, - "prompt_tokens_details": { - "cached_tokens": 0, - "audio_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0, - "audio_tokens": 0, - "accepted_prediction_tokens": 0, - "rejected_prediction_tokens": 0 - } - }, - "service_tier": "default", - "system_fingerprint": "fp_799e4ca3f1" + "id": "chatcmpl-C1XAKDTVYnmWS7tgvg7vPje00PIiy", + "object": "chat.completion", + "created": 1754481852, + "model": "gpt-4.1-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "I am about to call the relevant function to list all workspaces for the user admin and provide their workspace IDs.\n\nExecuting the function call now.", + "tool_calls": [ + { + "id": "call_aEuQAWKQYInC6fQ4z0iatdVP", + "type": "function", + "function": { + "name": "__mcp__coder_coder_list_workspaces", + "arguments": "{\"owner\":\"admin\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 4862, + "completion_tokens": 52, + "total_tokens": 4914, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_51e1070cf2" } -- non-streaming/tool-call -- { - "id": "chatcmpl-C0qiwr0OioIFZvyn4wcsrMy5cvncY", + "id": "chatcmpl-C1XANLwdflVxAjKOjbMP3LJxSlXsS", "object": "chat.completion", - "created": 1754318706, + "created": 1754481855, "model": "gpt-4.1-2025-04-14", "choices": [ { "index": 0, "message": { "role": "assistant", - "content": "admin", + "content": "Here is the list of Coder workspace IDs for the user admin:\n\n- Workspace Name: bob\n- Workspace ID: dd711d5c-83c6-4c08-a0af-b73055906e8c\n\nLet me know if you need more details or actions on this workspace!", "refusal": null, "annotations": [] }, @@ -138,9 +274,9 @@ data: [DONE] } ], "usage": { - "prompt_tokens": 5093, - "completion_tokens": 1, - "total_tokens": 5094, + "prompt_tokens": 5056, + "completion_tokens": 63, + "total_tokens": 5119, "prompt_tokens_details": { "cached_tokens": 4864, "audio_tokens": 0 @@ -153,6 +289,6 @@ data: [DONE] } }, "service_tier": "default", - "system_fingerprint": "fp_799e4ca3f1" + "system_fingerprint": "fp_51e1070cf2" } diff --git a/aibridged/mcp.go b/aibridged/mcp.go index de1bd356d5e3f..2cff5dee85186 100644 --- a/aibridged/mcp.go +++ b/aibridged/mcp.go @@ -145,17 +145,19 @@ func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]*MCPTool, return out, nil } +// EncodeToolID namespaces the given tool name to disambiguate against other tools. func EncodeToolID(server, tool string) string { return fmt.Sprintf("%s%s%s%s", MCPPrefix, server, MCPDelimiter, tool) } -func DecodeToolID(id string) (string, string, error) { +// DecodeToolID strips the namespacing from EncodeToolID. +func DecodeToolID(id string) (server string, tool string, err error) { _, name, ok := strings.Cut(id, MCPPrefix) if !ok { return "", "", xerrors.Errorf("unable to decode %q, prefix %q not found", id, MCPPrefix) } - server, tool, ok := strings.Cut(name, MCPDelimiter) + server, tool, ok = strings.Cut(name, MCPDelimiter) if !ok { return "", "", xerrors.Errorf("unable to decode %q, delimiter %q not found", id, MCPDelimiter) } From e6a4e3faaf5ab353c1bd89079e87e2edb1e7b165 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 7 Aug 2025 11:42:23 +0200 Subject: [PATCH 51/61] WIP: bridge refactor Signed-off-by: Danny Kopping --- aibridged/bridge.go | 58 +++++-- aibridged/openai.go | 35 +++++ aibridged/provider.go | 7 + aibridged/provider_openai.go | 53 +++++++ aibridged/session.go | 51 ++----- aibridged/session_openai.go | 289 +++++++++++++++++++++++++++++++++++ aibridged/tool_manager.go | 76 +++++++++ aibridged/tracker.go | 81 ++++++++++ aibridged/tracking.go | 41 ++--- coderd/aibridge.go | 6 +- 10 files changed, 614 insertions(+), 83 deletions(-) create mode 100644 aibridged/provider.go create mode 100644 aibridged/provider_openai.go create mode 100644 aibridged/session_openai.go create mode 100644 aibridged/tool_manager.go create mode 100644 aibridged/tracker.go diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 5ea077fc11571..087cd195c2a43 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -81,11 +81,54 @@ type Bridge struct { tools map[string]*MCPTool } -func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool), tools []*MCPTool) (*Bridge, error) { +func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool), tools map[string][]*MCPTool) (*Bridge, error) { var bridge Bridge mux := &http.ServeMux{} - mux.HandleFunc("/v1/chat/completions", bridge.proxyOpenAIRequest) + mux.HandleFunc("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { + prov := NewOpenAIProvider(cfg.OpenAI.BaseURL.String(), cfg.OpenAI.Key.String()) + + // TODO: everything is generic beyond this point... + + body, err := io.ReadAll(r.Body) + if err != nil { + if isConnectionError(err) { + logger.Debug(r.Context(), "client disconnected during request body read", slog.Error(err)) + return // Don't send error response if client already disconnected + } + logger.Error(r.Context(), "failed to read body", slog.Error(err)) + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + + req, err := prov.ParseRequest(body) + if err != nil { + logger.Error(r.Context(), "failed to parse request", slog.Error(err)) + http.Error(w, "failed to parse request", http.StatusBadRequest) + } + + var sess Session[ChatCompletionNewParamsWrapper] + if req.Stream { + sess = prov.NewAsynchronousSession(req) + } else { + sess = prov.NewSynchronousSession(req) + } + + coderdClient, ok := clientFn() + if !ok { + logger.Error(r.Context(), "could not acquire coderd client for tracking") + return + } + + sessID := sess.Init(logger, prov.baseURL, prov.key, NewDRPCTracker(coderdClient), NewInjectedToolManager(tools)) + defer func() { + if err := sess.Close(); err != nil { + logger.Warn(context.Background(), "failed to close session", slog.Error(err), slog.F("session_id", sessID), slog.F("kind", fmt.Sprintf("%T", sess))) + } + }() + + sess.Execute(req, w, r) // TODO: handle error? + }) mux.HandleFunc("/v1/messages", bridge.proxyAnthropicRequest) srv := &http.Server{ @@ -104,8 +147,10 @@ func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() bridge.logger = logger bridge.tools = make(map[string]*MCPTool, len(tools)) - for _, tool := range tools { - bridge.tools[tool.ID] = tool + for _, serverTools := range tools { + for _, tool := range serverTools { + bridge.tools[tool.ID] = tool + } } return &bridge, nil @@ -166,11 +211,6 @@ func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { b.trackUserPrompt(ctx, sessionID, "", in.Model, *prompt) } - // Prepend assistant message. - in.Messages = append([]openai.ChatCompletionMessageParamUnion{ - openai.SystemMessage("You are a helpful assistant that explicitly mentions when tool calls are about to be made."), - }, in.Messages...) - for _, tool := range b.tools { fn := openai.ChatCompletionToolParam{ Function: openai.FunctionDefinitionParam{ diff --git a/aibridged/openai.go b/aibridged/openai.go index 0f3ef7d963535..25bc157cbda32 100644 --- a/aibridged/openai.go +++ b/aibridged/openai.go @@ -1,9 +1,14 @@ package aibridged import ( + "regexp" + "strings" + "github.com/openai/openai-go" "github.com/openai/openai-go/packages/param" "github.com/tidwall/gjson" + "golang.org/x/xerrors" + "tailscale.com/types/ptr" ) // ChatCompletionNewParamsWrapper exists because the "stream" param is not included in openai.ChatCompletionNewParams. @@ -42,6 +47,36 @@ func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { return nil } +func (c *ChatCompletionNewParamsWrapper) LastUserPrompt() (*string, error) { + if c == nil { + return nil, xerrors.New("nil struct") + } + + if len(c.Messages) == 0 { + return nil, xerrors.New("no messages") + } + + var msg *openai.ChatCompletionUserMessageParam + for i := len(c.Messages) - 1; i >= 0; i-- { + m := c.Messages[i] + if m.OfUser != nil { + msg = m.OfUser + break + } + } + + if msg == nil { + return nil, nil + } + + userMessage := msg.Content.OfString.String() + if isCursor, _ := regexp.MatchString("", userMessage); isCursor { + userMessage = extractCursorUserQuery(userMessage) + } + + return ptr.To(strings.TrimSpace(userMessage)), nil +} + func sumUsage(ref, in openai.CompletionUsage) openai.CompletionUsage { return openai.CompletionUsage{ CompletionTokens: ref.CompletionTokens + in.CompletionTokens, diff --git a/aibridged/provider.go b/aibridged/provider.go new file mode 100644 index 0000000000000..bd75343c734cd --- /dev/null +++ b/aibridged/provider.go @@ -0,0 +1,7 @@ +package aibridged + +type Provider[Req any] interface { + ParseRequest(payload []byte) (*Req, error) + NewAsynchronousSession(*Req) Session[Req] + NewSynchronousSession(*Req) Session[Req] +} diff --git a/aibridged/provider_openai.go b/aibridged/provider_openai.go new file mode 100644 index 0000000000000..0593d3d240238 --- /dev/null +++ b/aibridged/provider_openai.go @@ -0,0 +1,53 @@ +package aibridged + +import ( + "encoding/json" + "os" + + "github.com/openai/openai-go" + "github.com/openai/openai-go/option" + "golang.org/x/xerrors" +) + +type OpenAIProvider struct { + baseURL, key string +} + +func NewOpenAIProvider(baseURL, key string) *OpenAIProvider { + return &OpenAIProvider{ + baseURL: baseURL, + key: key, + } +} + +func (*OpenAIProvider) ParseRequest(payload []byte) (*ChatCompletionNewParamsWrapper, error) { + var in ChatCompletionNewParamsWrapper + if err := json.Unmarshal(payload, &in); err != nil { + return nil, xerrors.Errorf("failed to unmarshal request: %w", err) + } + + return &in, nil +} + +func (p *OpenAIProvider) NewAsynchronousSession(req *ChatCompletionNewParamsWrapper) Session[ChatCompletionNewParamsWrapper] { + return &OpenAIStreamingSession{} +} +func (p *OpenAIProvider) NewSynchronousSession(req *ChatCompletionNewParamsWrapper) Session[ChatCompletionNewParamsWrapper] { + panic("not implemented") + +} + +func newOpenAIClient(baseURL, key string) openai.Client { + var opts []option.RequestOption + if key == "" { + key = os.Getenv("OPENAI_API_KEY") + } + opts = append(opts, option.WithAPIKey(key)) + if baseURL != "" { + opts = append(opts, option.WithBaseURL(baseURL)) + } + + opts = append(opts, option.WithMiddleware(LoggingMiddleware)) + + return openai.NewClient(opts...) +} diff --git a/aibridged/session.go b/aibridged/session.go index e7b8d842244a0..30c4e1155aaf8 100644 --- a/aibridged/session.go +++ b/aibridged/session.go @@ -1,50 +1,19 @@ package aibridged import ( - "fmt" - "sync/atomic" -) - -type OpenAIToolCall struct { - funcName string - args map[string]string -} + "net/http" -type OpenAIToolCallState int - -const ( - OpenAIToolCallNotReady OpenAIToolCallState = iota - OpenAIToolCallReady - OpenAIToolCallInProgress - OpenAIToolCallDone + "cdr.dev/slog" ) -func (o OpenAIToolCallState) String() string { - switch o { - case OpenAIToolCallNotReady: - return "not ready" - case OpenAIToolCallReady: - return "ready" - case OpenAIToolCallInProgress: - return "in-progress" - case OpenAIToolCallDone: - return "done" - default: - return fmt.Sprintf("UNKNOWN STATE: %d", o) - } -} - -type OpenAISession struct { - done atomic.Bool - // key = tool call ID - toolCallsRequired map[string]*OpenAIToolCall - toolCallsState map[string]OpenAIToolCallState - phantomEvents [][]byte +type Model struct { + Provider, ModelName string } -func NewOpenAISession() *OpenAISession { - return &OpenAISession{ - toolCallsRequired: make(map[string]*OpenAIToolCall), - toolCallsState: make(map[string]OpenAIToolCallState), - } +type Session[Req any] interface { + Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) (id string) + LastUserPrompt(req Req) (*string, error) + Model(req *Req) Model + Execute(req *Req, w http.ResponseWriter, r *http.Request) error + Close() error } diff --git a/aibridged/session_openai.go b/aibridged/session_openai.go new file mode 100644 index 0000000000000..2eea2e335df09 --- /dev/null +++ b/aibridged/session_openai.go @@ -0,0 +1,289 @@ +package aibridged + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "strings" + "sync" + + "github.com/google/uuid" + "github.com/openai/openai-go" + "github.com/openai/openai-go/packages/ssestream" + "golang.org/x/xerrors" + + "cdr.dev/slog" +) + +var _ Session[ChatCompletionNewParamsWrapper] = &OpenAIStreamingSession{} + +type OpenAISessionBase struct { + id string + baseURL, key string + logger slog.Logger + tracker Tracker + toolMgr ToolManager +} + +func (s *OpenAISessionBase) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { + s.id = uuid.NewString() + + s.logger = logger.With(slog.F("session_id", s.id)) + + s.baseURL = baseURL + s.key = key + + s.tracker = tracker + s.toolMgr = toolMgr + + return s.id +} + +func (*OpenAISessionBase) LastUserPrompt(req ChatCompletionNewParamsWrapper) (*string, error) { + return req.LastUserPrompt() +} + +type OpenAIStreamingSession struct { + OpenAISessionBase +} + +func (s *OpenAIStreamingSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { + return s.OpenAISessionBase.Init(logger.Named("openai.streaming"), baseURL, key, tracker, toolMgr) +} + +func (s *OpenAIStreamingSession) Execute(req *ChatCompletionNewParamsWrapper, w http.ResponseWriter, r *http.Request) error { + if req == nil { + return xerrors.Errorf("developer error: req is nil") + } + + // Include token usage. + req.StreamOptions.IncludeUsage = openai.Bool(true) + + // Inject tools. + for _, tool := range s.toolMgr.ListTools() { + fn := openai.ChatCompletionToolParam{ + Function: openai.FunctionDefinitionParam{ + Name: tool.ID, + Strict: openai.Bool(false), // TODO: configurable. + Description: openai.String(tool.Description), + Parameters: openai.FunctionParameters{ + "type": "object", + "properties": tool.Params, + // "additionalProperties": false, // Only relevant when strict=true. + }, + }, + } + + // Otherwise the request fails with "None is not of type 'array'" if a nil slice is given. + if len(tool.Required) > 0 { + // Must list ALL properties when strict=true. + fn.Function.Parameters["required"] = tool.Required + } + + req.Tools = append(req.Tools, fn) + } + + // Allow us to interrupt watch via cancel. + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + r = r.WithContext(ctx) // Rewire context for SSE cancellation. + + client := newOpenAIClient(s.baseURL, s.key) + + streamCtx, streamCancel := context.WithCancelCause(ctx) + defer streamCancel(xerrors.New("deferred")) + + events := newEventStream(openAIEventStream) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + defer func() { + if err := events.Close(streamCtx); err != nil { + s.logger.Error(ctx, "error closing stream", slog.Error(err)) + } + }() + + BasicSSESender(streamCtx, events, s.logger.Named("sse-sender")).ServeHTTP(w, r) + }() + + // TODO: implement parallel tool calls. + // TODO: don't send if not supported by model (i.e. o4-mini). + if len(req.Tools) > 0 { // If no tools are specified but this setting is set, it'll cause a 400 Bad Request. + req.ParallelToolCalls = openai.Bool(false) + } + + var ( + stream *ssestream.Stream[openai.ChatCompletionChunk] + cumulativeUsage openai.CompletionUsage + ) + for { + var pendingToolCalls []openai.FinishedChatCompletionToolCall + + stream = client.Chat.Completions.NewStreaming(ctx, req.ChatCompletionNewParams) + var acc openai.ChatCompletionAccumulator + for stream.Next() { + chunk := stream.Current() + acc.AddChunk(chunk) + + // fmt.Printf("[in]: %s\n", chunk.RawJSON()) + + shouldRelayChunk := true + if toolCall, ok := acc.JustFinishedToolCall(); ok { + // Don't intercept and handle builtin tools. + if s.toolMgr.GetTool(toolCall.Name) != nil { + pendingToolCalls = append(pendingToolCalls, toolCall) + // Don't relay this chunk back; we'll handle it transparently. + shouldRelayChunk = false + } else { + s.tracker.TrackToolUsage(ctx, s.id, chunk.ID, s.Model(req), toolCall.Name, toolCall.Arguments, false, nil) + } + } + + if len(pendingToolCalls) > 0 { + // Any chunks following a tool call invocation should not be relayed. + shouldRelayChunk = false + } + + cumulativeUsage = sumUsage(cumulativeUsage, chunk.Usage) + + if shouldRelayChunk { + // If usage information is available, relay the cumulative usage once all tool invocations have completed. + if chunk.Usage.CompletionTokens > 0 { + chunk.Usage = cumulativeUsage + } + + // Overwrite response identifier since proxy obscures injected tool call invocations. + chunk.ID = s.id + events.TrySend(ctx, chunk) + } + } + + // If the usage information is set, track it. + // The API will send usage information when the response terminates, which will happen if a tool call is invoked. + s.tracker.TrackTokensUsage(ctx, s.id, acc.ID, s.Model(req), cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ + "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, + "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, + "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, + "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, + "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, + "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, + }) + + if err := stream.Err(); err != nil { + s.logger.Error(ctx, "server stream error", slog.Error(err)) + var apierr *openai.Error + if errors.As(err, &apierr) { + events.TrySend(ctx, map[string]interface{}{ + // TODO: session ID? + "error": true, + "message": err.Error(), + }) + // http.Error(w, apierr.Message, apierr.StatusCode) + break + } else if isConnectionError(err) { + s.logger.Warn(ctx, "upstream connection error", slog.Error(err)) + } + + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if len(pendingToolCalls) == 0 { + break + } + + appendedPrevMsg := false + for _, tc := range pendingToolCalls { + tool := s.toolMgr.GetTool(tc.Name) + if tool == nil { + // Not an known tool, don't do anything. + s.logger.Warn(ctx, "pending tool call for non-managed tool, skipping...", slog.F("tool", tc.Name)) + continue + } + + // Only do this once. + if !appendedPrevMsg { + // Append the whole message from this stream as context since we'll be sending a new request with the tool results. + req.Messages = append(req.Messages, acc.Choices[len(acc.Choices)-1].Message.ToParam()) + appendedPrevMsg = true + } + + s.tracker.TrackToolUsage(ctx, s.id, acc.ID, s.Model(req), tc.Name, tc.Arguments, true, nil) + + var args map[string]any + if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil { + s.logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", tc.Name)) + } + + res, err := tool.Call(streamCtx, args) + if err != nil { + // Always provide a tool_result even if the tool call failed + errorResponse := map[string]interface{}{ + // TODO: session ID? + "error": true, + "message": err.Error(), + } + errorJSON, _ := json.Marshal(errorResponse) + req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + var out strings.Builder + if err := json.NewEncoder(&out).Encode(res); err != nil { + s.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) + // Always provide a tool_result even if encoding failed + // TODO: abstract. + errorResponse := map[string]interface{}{ + // TODO: session ID? + "error": true, + "message": err.Error(), + } + errorJSON, _ := json.Marshal(errorResponse) + req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + req.Messages = append(req.Messages, openai.ToolMessage(out.String(), tc.ID)) + } + } + + err := events.Close(streamCtx) + if err != nil { + s.logger.Error(ctx, "failed to close event stream", slog.Error(err)) + } + + wg.Wait() + + // Ensure we flush all the remaining data before ending. + flush(w) + + if err != nil { + streamCancel(xerrors.Errorf("stream err: %w", err)) + } else { + streamCancel(xerrors.New("gracefully done")) + } + + <-streamCtx.Done() + return nil +} + +func (s *OpenAIStreamingSession) Model(req *ChatCompletionNewParamsWrapper) Model { + var model string + if req == nil { + model = "?" + } else { + model = req.Model + } + + return Model{ + Provider: "openai", + ModelName: model, + } +} + +func (s *OpenAIStreamingSession) Close() error { + return nil // TODO: do we even need this? +} diff --git a/aibridged/tool_manager.go b/aibridged/tool_manager.go new file mode 100644 index 0000000000000..4a396f40a7685 --- /dev/null +++ b/aibridged/tool_manager.go @@ -0,0 +1,76 @@ +package aibridged + +import ( + "sync" +) + +type ToolManager interface { + AddTools(server string, tools []*MCPTool) + GetTool(name string) *MCPTool + ListTools() []*MCPTool +} + +var _ ToolManager = &InjectedToolManager{} + +// InjectedToolManager is responsible for all injected tools. +type InjectedToolManager struct { + mu sync.RWMutex + tools map[string]*MCPTool +} + +// +// +// +// +// TODO: need to inject tools along with their server name +// +// +// +// + +func NewInjectedToolManager(tools map[string][]*MCPTool) *InjectedToolManager { + tm := &InjectedToolManager{} + for server, val := range tools { + tm.AddTools(server, val) + } + return tm +} + +func (t *InjectedToolManager) AddTools(server string, tools []*MCPTool) { + t.mu.Lock() + defer t.mu.Unlock() + + if t.tools == nil { + t.tools = make(map[string]*MCPTool, len(tools)) + } + + for _, tool := range tools { + t.tools[EncodeToolID(server, tool.Name)] = tool + } +} + +func (t *InjectedToolManager) GetTool(name string) *MCPTool { + t.mu.RLock() + defer t.mu.RUnlock() + + if t.tools == nil { + return nil + } + + return t.tools[name] +} + +func (t *InjectedToolManager) ListTools() []*MCPTool { + t.mu.RLock() + defer t.mu.RUnlock() + + if t.tools == nil { + return nil + } + + var out []*MCPTool + for _, tool := range t.tools { + out = append(out, tool) + } + return out +} diff --git a/aibridged/tracker.go b/aibridged/tracker.go new file mode 100644 index 0000000000000..c2bed978d3b40 --- /dev/null +++ b/aibridged/tracker.go @@ -0,0 +1,81 @@ +package aibridged + +import ( + "context" + "encoding/json" + "fmt" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/aibridged/proto" +) + +type Metadata map[string]any + +type Tracker interface { + TrackTokensUsage(ctx context.Context, sessionID, msgID string, model Model, promptTokens, completionTokens int64, metadata Metadata) error + TrackPromptUsage(ctx context.Context, sessionID, msgID string, model Model, prompt string, metadata Metadata) error + TrackToolUsage(ctx context.Context, sessionID, msgID string, model Model, name string, args any, injected bool, metadata Metadata) error +} + +var _ Tracker = &DRPCTracker{} + +// DRPCTracker tracks usage by calling RPC endpoints on a given dRPC client. +type DRPCTracker struct { + client proto.DRPCAIBridgeDaemonClient +} + +func NewDRPCTracker(client proto.DRPCAIBridgeDaemonClient) *DRPCTracker { + return &DRPCTracker{client} +} + +func (d *DRPCTracker) TrackTokensUsage(ctx context.Context, sessionID, msgID string, model Model, promptTokens, completionTokens int64, metadata Metadata) error { + _, err := d.client.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + SessionId: sessionID, + MsgId: msgID, + InputTokens: promptTokens, + OutputTokens: completionTokens, + Model: fmt.Sprintf("%s.%s", model.Provider, model.ModelName), // TODO: make first-class type in proto. + // Other: metadata, // TODO: implement map in proto. + }) + return err +} + +func (d *DRPCTracker) TrackPromptUsage(ctx context.Context, sessionID, msgID string, model Model, prompt string, metadata Metadata) error { + _, err := d.client.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ + SessionId: sessionID, + MsgId: msgID, + Prompt: prompt, + Model: fmt.Sprintf("%s.%s", model.Provider, model.ModelName), // TODO: make first-class type in proto. + }) + return err +} + +func (d *DRPCTracker) TrackToolUsage(ctx context.Context, sessionID, msgID string, model Model, name string, args any, injected bool, metadata Metadata) error { + var ( + serialized []byte + err error + ) + + switch val := args.(type) { + case string: + serialized = []byte(val) + case []byte: + serialized = val + default: + serialized, err = json.Marshal(args) + if err != nil { + return xerrors.Errorf("marshal tool usage args: %w", err) + } + } + + _, err = d.client.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + SessionId: sessionID, + MsgId: msgID, + Tool: name, + Model: fmt.Sprintf("%s.%s", model.Provider, model.ModelName), // TODO: make first-class type in proto. + Input: string(serialized), + Injected: injected, + }) + return err +} diff --git a/aibridged/tracking.go b/aibridged/tracking.go index e22692c7fd3b7..00e7056929a4a 100644 --- a/aibridged/tracking.go +++ b/aibridged/tracking.go @@ -5,46 +5,25 @@ import ( "strings" "github.com/anthropics/anthropic-sdk-go" - "github.com/openai/openai-go" "golang.org/x/xerrors" "tailscale.com/types/ptr" ) +// +// +// +// +// TODO: decompose this file into more appropriate locations. +// +// +// +// + type UsageExtractor interface { LastUserPrompt() (*string, error) LastToolCalls() ([]string, error) } -func (c *ChatCompletionNewParamsWrapper) LastUserPrompt() (*string, error) { - if c == nil { - return nil, xerrors.New("nil struct") - } - - if len(c.Messages) == 0 { - return nil, xerrors.New("no messages") - } - - var msg *openai.ChatCompletionUserMessageParam - for i := len(c.Messages) - 1; i >= 0; i-- { - m := c.Messages[i] - if m.OfUser != nil { - msg = m.OfUser - break - } - } - - if msg == nil { - return nil, nil - } - - userMessage := msg.Content.OfString.String() - if isCursor, _ := regexp.MatchString("", userMessage); isCursor { - userMessage = extractCursorUserQuery(userMessage) - } - - return ptr.To(strings.TrimSpace(userMessage)), nil -} - func (b *BetaMessageNewParamsWrapper) LastUserPrompt() (*string, error) { if b == nil { return nil, xerrors.New("nil struct") diff --git a/coderd/aibridge.go b/coderd/aibridge.go index e550074ea0335..2f9653ec20f10 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -99,7 +99,7 @@ func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, cli return val, nil } -func (api *API) fetchTools(ctx context.Context, logger slog.Logger, key string) ([]*aibridged.MCPTool, error) { +func (api *API) fetchTools(ctx context.Context, logger slog.Logger, key string) (map[string][]*aibridged.MCPTool, error) { url := api.DeploymentValues.AccessURL.String() + "/api/experimental/mcp/http" coderMCP, err := aibridged.NewMCPToolBridge("coder", url, map[string]string{ "Coder-Session-Token": key, @@ -126,5 +126,7 @@ func (api *API) fetchTools(ctx context.Context, logger slog.Logger, key string) return nil, xerrors.Errorf("MCP proxy init: %w", err) } - return coderMCP.ListTools(), nil + return map[string][]*aibridged.MCPTool{ + "coder": coderMCP.ListTools(), + }, nil } From ce5e7e3f7cba71cb8dd7cfec5377dfa82c422e44 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 7 Aug 2025 16:35:52 +0200 Subject: [PATCH 52/61] refactored openai into new structure Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 24 +- aibridged/anthropic.go | 60 +++ aibridged/bridge.go | 508 ++------------------- aibridged/bridge_integration_test.go | 36 +- aibridged/provider.go | 4 +- aibridged/provider_openai.go | 53 --- aibridged/provider_openai_chat.go | 60 +++ aibridged/session.go | 9 +- aibridged/session_openai.go | 289 ------------ aibridged/session_openai_chat_base.go | 94 ++++ aibridged/session_openai_chat_blocking.go | 215 +++++++++ aibridged/session_openai_chat_streaming.go | 239 ++++++++++ aibridged/tracking.go | 80 ---- coderd/aibridge.go | 2 +- 14 files changed, 753 insertions(+), 920 deletions(-) delete mode 100644 aibridged/provider_openai.go create mode 100644 aibridged/provider_openai_chat.go delete mode 100644 aibridged/session_openai.go create mode 100644 aibridged/session_openai_chat_base.go create mode 100644 aibridged/session_openai_chat_blocking.go create mode 100644 aibridged/session_openai_chat_streaming.go delete mode 100644 aibridged/tracking.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 4b9893bb88652..9d9a1d77dca39 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -137,15 +137,15 @@ connectLoop: } } -func (s *Server) Client() (proto.DRPCAIBridgeDaemonClient, bool) { +func (s *Server) Client() (proto.DRPCAIBridgeDaemonClient, error) { select { case <-s.closeContext.Done(): - return nil, false + return nil, xerrors.New("context closed") case <-s.shuttingDownCh: // Shutting down should return a nil client and unblock - return nil, false + return nil, xerrors.New("shutting down") case client := <-s.clientCh: - return client, true + return client, nil } } @@ -179,7 +179,7 @@ func (s *Server) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageReq return out, nil } -// TODO: direct copy/paste from provisionerd, abstract into common util. +// NOTE: mostly copypasta from provisionerd; might be work abstracting. func retryable(err error) bool { return xerrors.Is(err, yamux.ErrSessionShutdown) || xerrors.Is(err, io.EOF) || xerrors.Is(err, fasthttputil.ErrInmemoryListenerClosed) || // annoyingly, dRPC sometimes returns context.Canceled if the transport was closed, even if the context for @@ -190,15 +190,19 @@ func retryable(err error) bool { // clientDoWithRetries runs the function f with a client, and retries with // backoff until either the error returned is not retryable() or the context // expires. -// TODO: direct copy/paste from provisionerd, abstract into common util. +// NOTE: mostly copypasta from provisionerd; might be work abstracting. func clientDoWithRetries[T any](ctx context.Context, - getClient func() (proto.DRPCAIBridgeDaemonClient, bool), + getClient func() (proto.DRPCAIBridgeDaemonClient, error), f func(context.Context, proto.DRPCAIBridgeDaemonClient) (T, error), ) (ret T, _ error) { for retrier := retry.New(25*time.Millisecond, 5*time.Second); retrier.Wait(ctx); { - client, ok := getClient() - if !ok { - continue + var empty T + client, err := getClient() + if err != nil { + if retryable(err) { + continue + } + return empty, err } resp, err := f(ctx, client) if retryable(err) { diff --git a/aibridged/anthropic.go b/aibridged/anthropic.go index fb24b07b0074e..99838c711ecf6 100644 --- a/aibridged/anthropic.go +++ b/aibridged/anthropic.go @@ -2,10 +2,14 @@ package aibridged import ( "encoding/json" + "regexp" + "strings" "github.com/anthropics/anthropic-sdk-go" ant_param "github.com/anthropics/anthropic-sdk-go/packages/param" "github.com/tidwall/gjson" + "golang.org/x/xerrors" + "tailscale.com/types/ptr" ) type streamer interface { @@ -161,6 +165,62 @@ func (b *BetaMessageNewParamsWrapper) UnmarshalJSON(raw []byte) error { b.Stream = extractStreamFlag(raw) return nil } + func (b *BetaMessageNewParamsWrapper) UseStreaming() bool { return b.Stream } + +func (b *BetaMessageNewParamsWrapper) LastUserPrompt() (*string, error) { + if b == nil { + return nil, xerrors.New("nil struct") + } + + if len(b.Messages) == 0 { + return nil, xerrors.New("no messages") + } + + var userMessage string + for i := len(b.Messages) - 1; i >= 0; i-- { + m := b.Messages[i] + if m.Role != anthropic.BetaMessageParamRoleUser { + continue + } + if len(m.Content) == 0 { + continue + } + + for j := len(m.Content) - 1; j >= 0; j-- { + if textContent := m.Content[j].GetText(); textContent != nil { + userMessage = *textContent + } + + // Ignore internal Claude Code prompts. + if userMessage == "test" || + strings.Contains(userMessage, "") { + userMessage = "" + continue + } + + // Handle Cursor-specific formatting by extracting content from tags + if isCursor, _ := regexp.MatchString("", userMessage); isCursor { + userMessage = extractCursorUserQuery(userMessage) + } + return ptr.To(strings.TrimSpace(userMessage)), nil + } + } + + return nil, nil +} + +func extractCursorUserQuery(message string) string { + pat := regexp.MustCompile(`(?P[\s\S]*?)`) + match := pat.FindStringSubmatch(message) + if match != nil { + // Get the named group by index + contentIndex := pat.SubexpIndex("content") + if contentIndex != -1 { + message = match[contentIndex] + } + } + return strings.TrimSpace(message) +} diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 087cd195c2a43..34cd08420ddbb 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -1,14 +1,12 @@ package aibridged import ( - "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" - "net/http/httputil" "net/url" "os" "strings" @@ -20,9 +18,6 @@ import ( ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" "github.com/google/uuid" "github.com/mark3labs/mcp-go/mcp" - "github.com/openai/openai-go" - oai_option "github.com/openai/openai-go/option" - "github.com/openai/openai-go/packages/ssestream" "github.com/openai/openai-go/shared/constant" "golang.org/x/xerrors" @@ -75,21 +70,15 @@ type Bridge struct { cfg codersdk.AIBridgeConfig httpSrv *http.Server - clientFn func() (proto.DRPCAIBridgeDaemonClient, bool) + clientFn func() (proto.DRPCAIBridgeDaemonClient, error) logger slog.Logger tools map[string]*MCPTool } -func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, bool), tools map[string][]*MCPTool) (*Bridge, error) { - var bridge Bridge - - mux := &http.ServeMux{} - mux.HandleFunc("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { - prov := NewOpenAIProvider(cfg.OpenAI.BaseURL.String(), cfg.OpenAI.Key.String()) - - // TODO: everything is generic beyond this point... - +func handleOpenAI(provider *OpenAIChatProvider, drpcClient proto.DRPCAIBridgeDaemonClient, tools map[string][]*MCPTool, logger slog.Logger) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Read and parse request. body, err := io.ReadAll(r.Body) if err != nil { if isConnectionError(err) { @@ -100,35 +89,49 @@ func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() http.Error(w, "failed to read body", http.StatusInternalServerError) return } - - req, err := prov.ParseRequest(body) + req, err := provider.ParseRequest(body) if err != nil { logger.Error(r.Context(), "failed to parse request", slog.Error(err)) http.Error(w, "failed to parse request", http.StatusBadRequest) + return } - var sess Session[ChatCompletionNewParamsWrapper] + // Create a new session. + var sess Session if req.Stream { - sess = prov.NewAsynchronousSession(req) + sess = provider.NewStreamingSession(req) } else { - sess = prov.NewSynchronousSession(req) + sess = provider.NewBlockingSession(req) } - coderdClient, ok := clientFn() - if !ok { - logger.Error(r.Context(), "could not acquire coderd client for tracking") - return - } + sessID := sess.Init(logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) + logger.Debug(context.Background(), "starting openai session", slog.F("session_id", sessID)) - sessID := sess.Init(logger, prov.baseURL, prov.key, NewDRPCTracker(coderdClient), NewInjectedToolManager(tools)) defer func() { if err := sess.Close(); err != nil { logger.Warn(context.Background(), "failed to close session", slog.Error(err), slog.F("session_id", sessID), slog.F("kind", fmt.Sprintf("%T", sess))) } }() - sess.Execute(req, w, r) // TODO: handle error? - }) + // Process the request. + if err := sess.ProcessRequest(w, r); err != nil { + logger.Error(r.Context(), "session execution failed", slog.Error(err)) + } + } +} + +func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, error), tools map[string][]*MCPTool) (*Bridge, error) { + var bridge Bridge + + openAIProvider := NewOpenAIChatProvider(cfg.OpenAI.BaseURL.String(), cfg.OpenAI.Key.String()) + + drpcClient, err := clientFn() + if err != nil { + return nil, xerrors.Errorf("could not acquire coderd client for tracking: %w", err) + } + + mux := &http.ServeMux{} + mux.HandleFunc("/v1/chat/completions", handleOpenAI(openAIProvider, drpcClient, tools, logger.Named("openai"))) mux.HandleFunc("/v1/messages", bridge.proxyAnthropicRequest) srv := &http.Server{ @@ -169,429 +172,6 @@ func (b *Bridge) Handler() http.Handler { return b.httpSrv.Handler } -// proxyOpenAIRequest intercepts, filters, augments, and delivers requests & responses from client to upstream and back. -// -// References: -// - https://platform.openai.com/docs/api-reference/chat-streaming -func (b *Bridge) proxyOpenAIRequest(w http.ResponseWriter, r *http.Request) { - sessionID := uuid.NewString() - b.logger.Info(r.Context(), "openai request started", slog.F("session_id", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) - _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) - - defer func() { - b.logger.Info(r.Context(), "openai request ended", slog.F("session_id", sessionID)) - _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) - }() - - // Allow us to interrupt watch via cancel. - ctx, cancel := context.WithCancel(r.Context()) - defer cancel() - r = r.WithContext(ctx) // Rewire context for SSE cancellation. - - body, err := io.ReadAll(r.Body) - if err != nil { - if isConnectionError(err) { - b.logger.Debug(r.Context(), "client disconnected during request body read", slog.Error(err)) - return // Don't send error response if client already disconnected - } - b.logger.Error(r.Context(), "failed to read body", slog.Error(err)) - http.Error(w, "failed to read body", http.StatusInternalServerError) - return - } - - var in ChatCompletionNewParamsWrapper - if err = json.Unmarshal(body, &in); err != nil { - b.logger.Error(r.Context(), "failed to unmarshal request", slog.Error(err)) - http.Error(w, "failed to unmarshal request", http.StatusInternalServerError) - return - } - - prompt, err := in.LastUserPrompt() // TODO: error handling. - if prompt != nil { - b.trackUserPrompt(ctx, sessionID, "", in.Model, *prompt) - } - - for _, tool := range b.tools { - fn := openai.ChatCompletionToolParam{ - Function: openai.FunctionDefinitionParam{ - Name: tool.ID, - Strict: openai.Bool(false), // TODO: configurable. - Description: openai.String(tool.Description), - Parameters: openai.FunctionParameters{ - "type": "object", - "properties": tool.Params, - // "additionalProperties": false, // Only relevant when strict=true. - }, - }, - } - - // Otherwise the request fails with "None is not of type 'array'" if a nil slice is given. - if len(tool.Required) > 0 { - // Must list ALL properties when strict=true. - fn.Function.Parameters["required"] = tool.Required - } - - in.Tools = append(in.Tools, fn) - } - - // Configure OpenAI client with authentication - var opts []oai_option.RequestOption - apiKey := b.cfg.OpenAI.Key.String() - if apiKey == "" { - apiKey = os.Getenv("OPENAI_API_KEY") - } - opts = append(opts, oai_option.WithAPIKey(apiKey)) - if baseURL := b.cfg.OpenAI.BaseURL.String(); baseURL != "" { - opts = append(opts, oai_option.WithBaseURL(baseURL)) - } - - opts = append(opts, oai_option.WithMiddleware(LoggingMiddleware)) - - client := openai.NewClient(opts...) - req := in.ChatCompletionNewParams - - if in.Stream { - in.StreamOptions.IncludeUsage = openai.Bool(true) - - streamCtx, streamCancel := context.WithCancelCause(ctx) - defer streamCancel(xerrors.New("deferred")) - - events := newEventStream(openAIEventStream) - - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - defer func() { - if err := events.Close(streamCtx); err != nil { - b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("session_id", sessionID)) - } - }() - - BasicSSESender(streamCtx, events, b.logger.Named("sse-sender")).ServeHTTP(w, r) - }() - - // TODO: implement parallel tool calls. - // TODO: don't send if not supported by model (i.e. o4-mini). - req.ParallelToolCalls = openai.Bool(false) - - var ( - stream *ssestream.Stream[openai.ChatCompletionChunk] - cumulativeUsage openai.CompletionUsage - ) - for { - var pendingToolCalls []openai.FinishedChatCompletionToolCall - - stream = client.Chat.Completions.NewStreaming(ctx, req) - var acc openai.ChatCompletionAccumulator - for stream.Next() { - chunk := stream.Current() - acc.AddChunk(chunk) - - // fmt.Printf("[in]: %s\n", chunk.RawJSON()) - - shouldRelayChunk := true - if toolCall, ok := acc.JustFinishedToolCall(); ok { - // Don't intercept and handle builtin tools. - if b.isInjectedTool(toolCall.Name) { - pendingToolCalls = append(pendingToolCalls, toolCall) - // Don't relay this chunk back; we'll handle it transparently. - shouldRelayChunk = false - } else { - b.trackToolUsage(ctx, sessionID, chunk.ID, in.Model, toolCall.Name, toolCall.Arguments, false) - } - } - - if len(pendingToolCalls) > 0 { - // Any chunks following a tool call invocation should not be relayed. - shouldRelayChunk = false - } - - cumulativeUsage = sumUsage(cumulativeUsage, chunk.Usage) - - if shouldRelayChunk { - // If usage information is available, relay the cumulative usage once all tool invocations have completed. - if chunk.Usage.CompletionTokens > 0 { - chunk.Usage = cumulativeUsage - } - - // Overwrite response identifier since proxy obscures injected tool call invocations. - chunk.ID = sessionID - events.TrySend(ctx, chunk) - - // fmt.Printf("\t[out]: %s\n", chunk.RawJSON()) - } - } - - // If the usage information is set, track it. - // The API will send usage information when the response terminates, which will happen if a tool call is invoked. - b.trackTokenUsage(ctx, sessionID, acc.ID, string(acc.Model), cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, map[string]int64{ - "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, - "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, - "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, - "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, - "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, - "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, - }) - - if err := stream.Err(); err != nil { - b.logger.Error(ctx, "server stream error", slog.Error(err)) - var apierr *openai.Error - if errors.As(err, &apierr) { - events.TrySend(ctx, map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - }) - // http.Error(w, apierr.Message, apierr.StatusCode) - break - } else if isConnectionError(err) { - b.logger.Warn(ctx, "upstream connection error", slog.Error(err)) - } - - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - if len(pendingToolCalls) == 0 { - break - } - - appendedPrevMsg := false - for _, tc := range pendingToolCalls { - if !b.isInjectedTool(tc.Name) { - // Not an MCP proxy call, don't do anything. - continue - } - - // Only do this once. - if !appendedPrevMsg { - // Append the whole message from this stream as context since we'll be sending a new request with the tool results. - req.Messages = append(req.Messages, acc.Choices[len(acc.Choices)-1].Message.ToParam()) - appendedPrevMsg = true - } - - b.trackToolUsage(ctx, sessionID, acc.ID, in.Model, tc.Name, tc.Arguments, true) - - var args map[string]any - if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil { - b.logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", tc.Name)) - } - - res, err := b.tools[tc.Name].Call(streamCtx, args) - if err != nil { - // Always provide a tool_result even if the tool call failed - errorResponse := map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - } - errorJSON, _ := json.Marshal(errorResponse) - req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - var out strings.Builder - if err := json.NewEncoder(&out).Encode(res); err != nil { - b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) - // Always provide a tool_result even if encoding failed - // TODO: abstract. - errorResponse := map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - } - errorJSON, _ := json.Marshal(errorResponse) - req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - req.Messages = append(req.Messages, openai.ToolMessage(out.String(), tc.ID)) - } - } - - err = events.Close(streamCtx) - if err != nil { - b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) - } - - wg.Wait() - - // Ensure we flush all the remaining data before ending. - flush(w) - - if err != nil { - streamCancel(xerrors.Errorf("stream err: %w", err)) - } else { - streamCancel(xerrors.New("gracefully done")) - } - - <-streamCtx.Done() - } else { - // Non-streaming case with tool calling support - var cumulativeUsage openai.CompletionUsage - var ( - completion *openai.ChatCompletion - err error - ) - - for { - completion, err = client.Chat.Completions.New(ctx, req) - if err != nil { - if isConnectionError(err) { - b.logger.Debug(ctx, "upstream connection closed", slog.Error(err)) - return // Don't send error response if client already disconnected - } - var apierr *openai.Error - if errors.As(err, &apierr) { - http.Error(w, apierr.Message, apierr.StatusCode) - return - } - - b.logger.Error(ctx, "chat completion failed", slog.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Track cumulative usage - cumulativeUsage = sumUsage(cumulativeUsage, completion.Usage) - - // Track token usage - b.trackTokenUsage(ctx, sessionID, completion.ID, completion.Model, cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, map[string]int64{ - "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, - "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, - "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, - "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, - "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, - "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, - }) - - // Check if we have tool calls to process - var pendingToolCalls []openai.ChatCompletionMessageToolCall - if len(completion.Choices) > 0 && completion.Choices[0].Message.ToolCalls != nil { - for _, toolCall := range completion.Choices[0].Message.ToolCalls { - if b.isInjectedTool(toolCall.Function.Name) { - pendingToolCalls = append(pendingToolCalls, toolCall) - } else { - b.trackToolUsage(ctx, sessionID, completion.ID, in.Model, toolCall.Function.Name, toolCall.Function.Arguments, false) - } - } - } - - // If no injected tool calls, we're done - if len(pendingToolCalls) == 0 { - break - } - - appendedPrevMsg := false - // Process each pending tool call - for _, tc := range pendingToolCalls { - fn := tc.Function.Name - if !b.isInjectedTool(fn) { - // Not an MCP proxy call, don't do anything. - continue - } - // Only do this once. - if !appendedPrevMsg { - // Append the whole message from this stream as context since we'll be sending a new request with the tool results. - req.Messages = append(req.Messages, completion.Choices[0].Message.ToParam()) - appendedPrevMsg = true - } - - b.trackToolUsage(ctx, sessionID, completion.ID, in.Model, fn, tc.Function.Arguments, true) - - var ( - args map[string]string - buf bytes.Buffer - ) - _ = json.NewEncoder(&buf).Encode(tc.Function.Arguments) - _ = json.NewDecoder(&buf).Decode(&args) - res, err := b.tools[fn].Call(ctx, args) - if err != nil { - // Always provide a tool result even if the tool call failed - errorResponse := map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - } - errorJSON, _ := json.Marshal(errorResponse) - req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - var out strings.Builder - if err := json.NewEncoder(&out).Encode(res); err != nil { - b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) - // Always provide a tool result even if encoding failed - errorResponse := map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - } - errorJSON, _ := json.Marshal(errorResponse) - req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - req.Messages = append(req.Messages, openai.ToolMessage(out.String(), tc.ID)) - } - } - - if completion == nil { - return - } - - // Overwrite response identifier since proxy obscures injected tool call invocations. - completion.ID = sessionID - - // Update the cumulative usage in the final response - if completion.Usage.CompletionTokens > 0 { - completion.Usage = cumulativeUsage - } - - w.Header().Set("Content-Type", "application/json") - out, err := json.Marshal(completion) - if err != nil { - // TODO: abstract. - errorResponse := map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": fmt.Sprintf("failed to marshal response: %s", err), - } - out, _ = json.Marshal(errorResponse) - w.WriteHeader(http.StatusInternalServerError) - } else { - w.WriteHeader(http.StatusOK) - } - - _, _ = w.Write(out) - } -} - -func LoggingMiddleware(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { - reqOut, _ := httputil.DumpRequest(req, true) - - // Forward the request to the next handler - res, err = next(req) - - isSmallFastModel := strings.Contains(string(reqOut), "3-5-haiku") - if isSmallFastModel { - return res, err - } - - fmt.Printf("[req] %s\n", reqOut) - - // Handle stuff after the request - if err != nil { - return res, err - } - - respOut, _ := httputil.DumpResponse(res, true) - fmt.Printf("[resp] %s\n", respOut) - - return res, err -} - // TODO: track cumulative usage when tool invocations are executed; see OpenAI implementation. func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { sessionID := uuid.NewString() @@ -714,7 +294,7 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { if reqBetaHeader := r.Header.Get("anthropic-beta"); strings.TrimSpace(reqBetaHeader) != "" { opts = append(opts, option.WithHeader("anthropic-beta", reqBetaHeader)) } - opts = append(opts, option.WithMiddleware(LoggingMiddleware)) + // opts = append(opts, option.WithMiddleware(LoggingMiddleware)) apiKey := b.cfg.Anthropic.Key.String() if apiKey == "" { @@ -1233,9 +813,9 @@ func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { } func (b *Bridge) trackToolUsage(ctx context.Context, sessionID, msgID, model, toolName string, toolInput interface{}, injected bool) { - coderdClient, ok := b.clientFn() - if !ok { - b.logger.Error(ctx, "could not acquire coderd client for tool usage tracking") + coderdClient, err := b.clientFn() + if err != nil { + b.logger.Error(ctx, "could not acquire coderd client for tool usage tracking", slog.Error(err)) return } @@ -1265,7 +845,7 @@ func (b *Bridge) trackToolUsage(ctx context.Context, sessionID, msgID, model, to } } - _, err := coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ + _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ SessionId: sessionID, MsgId: msgID, Model: model, @@ -1279,13 +859,13 @@ func (b *Bridge) trackToolUsage(ctx context.Context, sessionID, msgID, model, to } func (b *Bridge) trackUserPrompt(ctx context.Context, sessionID, msgID, model, prompt string) { - coderdClient, ok := b.clientFn() - if !ok { - b.logger.Error(ctx, "could not acquire coderd client for user prompt tracking") + coderdClient, err := b.clientFn() + if err != nil { + b.logger.Error(ctx, "could not acquire coderd client for user prompt tracking", slog.Error(err)) return } - _, err := coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ + _, err = coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ SessionId: sessionID, MsgId: msgID, Model: model, @@ -1297,13 +877,13 @@ func (b *Bridge) trackUserPrompt(ctx context.Context, sessionID, msgID, model, p } func (b *Bridge) trackTokenUsage(ctx context.Context, sessionID, msgID, model string, inputTokens, outputTokens int64, other map[string]int64) { - coderdClient, ok := b.clientFn() - if !ok { - b.logger.Error(ctx, "could not acquire coderd client for token usage tracking") + coderdClient, err := b.clientFn() + if err != nil { + b.logger.Error(ctx, "could not acquire coderd client for token usage tracking", slog.Error(err)) return } - _, err := coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ + _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ SessionId: sessionID, MsgId: msgID, Model: model, diff --git a/aibridged/bridge_integration_test.go b/aibridged/bridge_integration_test.go index 57b80feb8429d..17207ede87a7e 100644 --- a/aibridged/bridge_integration_test.go +++ b/aibridged/bridge_integration_test.go @@ -125,8 +125,8 @@ func TestAnthropicMessages(t *testing.T) { BaseURL: serpent.String(srv.URL), Key: serpent.String(sessionToken), }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return coderdClient, true + }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + return coderdClient, nil }, nil) require.NoError(t, err) @@ -229,8 +229,8 @@ func TestOpenAIChatCompletions(t *testing.T) { BaseURL: serpent.String(srv.URL), Key: serpent.String(sessionToken), }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return coderdClient, true + }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + return coderdClient, nil }, nil) require.NoError(t, err) @@ -302,8 +302,8 @@ func TestSimple(t *testing.T) { BaseURL: serpent.String(addr), Key: serpent.String(sessionToken), }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return client, true + }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + return client, nil }, nil) }, getResponseIDFunc: func(streaming bool, resp *http.Response) (string, error) { @@ -349,8 +349,8 @@ func TestSimple(t *testing.T) { BaseURL: serpent.String(addr), Key: serpent.String(sessionToken), }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return client, true + }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + return client, nil }, nil) }, getResponseIDFunc: func(streaming bool, resp *http.Response) (string, error) { @@ -461,7 +461,7 @@ func TestSimple(t *testing.T) { } // setupMCPToolsForTest creates a mock MCP server, initializes the MCP bridge, and returns the tools -func setupMCPToolsForTest(t *testing.T) []*aibridged.MCPTool { +func setupMCPToolsForTest(t *testing.T) map[string][]*aibridged.MCPTool { t.Helper() // Setup Coder MCP integration @@ -477,7 +477,9 @@ func setupMCPToolsForTest(t *testing.T) []*aibridged.MCPTool { tools := mcpBridge.ListTools() require.NotEmpty(t, tools) - return tools + return map[string][]*aibridged.MCPTool{ + "coder": tools, + } } // TestInjectedTool is an abstracted test function for "single injected tool" scenarios @@ -491,14 +493,14 @@ func TestInjectedTool(t *testing.T) { testCases := []struct { name string fixture []byte - configureFunc func(string, proto.DRPCAIBridgeDaemonClient, []*aibridged.MCPTool) (*aibridged.Bridge, error) + configureFunc func(string, proto.DRPCAIBridgeDaemonClient, map[string][]*aibridged.MCPTool) (*aibridged.Bridge, error) getResponseContentFunc func(bool, *http.Response) (string, error) createRequest func(*testing.T, string, []byte) *http.Request }{ { name: "anthropic", fixture: antSingleInjectedTool, - configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools []*aibridged.MCPTool) (*aibridged.Bridge, error) { + configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools map[string][]*aibridged.MCPTool) (*aibridged.Bridge, error) { logger := testutil.Logger(t) return aibridged.NewBridge(codersdk.AIBridgeConfig{ Daemons: 1, @@ -506,8 +508,8 @@ func TestInjectedTool(t *testing.T) { BaseURL: serpent.String(addr), Key: serpent.String(sessionToken), }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return client, true + }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + return client, nil }, tools) }, getResponseContentFunc: func(streaming bool, resp *http.Response) (string, error) { @@ -558,7 +560,7 @@ func TestInjectedTool(t *testing.T) { { name: "openai", fixture: oaiSingleInjectedTool, - configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools []*aibridged.MCPTool) (*aibridged.Bridge, error) { + configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools map[string][]*aibridged.MCPTool) (*aibridged.Bridge, error) { logger := testutil.Logger(t) return aibridged.NewBridge(codersdk.AIBridgeConfig{ Daemons: 1, @@ -566,8 +568,8 @@ func TestInjectedTool(t *testing.T) { BaseURL: serpent.String(addr), Key: serpent.String(sessionToken), }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, bool) { - return client, true + }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + return client, nil }, tools) }, getResponseContentFunc: func(streaming bool, resp *http.Response) (string, error) { diff --git a/aibridged/provider.go b/aibridged/provider.go index bd75343c734cd..1d45520ac7d6f 100644 --- a/aibridged/provider.go +++ b/aibridged/provider.go @@ -2,6 +2,6 @@ package aibridged type Provider[Req any] interface { ParseRequest(payload []byte) (*Req, error) - NewAsynchronousSession(*Req) Session[Req] - NewSynchronousSession(*Req) Session[Req] + NewStreamingSession(*Req) Session + NewBlockingSession(*Req) Session } diff --git a/aibridged/provider_openai.go b/aibridged/provider_openai.go deleted file mode 100644 index 0593d3d240238..0000000000000 --- a/aibridged/provider_openai.go +++ /dev/null @@ -1,53 +0,0 @@ -package aibridged - -import ( - "encoding/json" - "os" - - "github.com/openai/openai-go" - "github.com/openai/openai-go/option" - "golang.org/x/xerrors" -) - -type OpenAIProvider struct { - baseURL, key string -} - -func NewOpenAIProvider(baseURL, key string) *OpenAIProvider { - return &OpenAIProvider{ - baseURL: baseURL, - key: key, - } -} - -func (*OpenAIProvider) ParseRequest(payload []byte) (*ChatCompletionNewParamsWrapper, error) { - var in ChatCompletionNewParamsWrapper - if err := json.Unmarshal(payload, &in); err != nil { - return nil, xerrors.Errorf("failed to unmarshal request: %w", err) - } - - return &in, nil -} - -func (p *OpenAIProvider) NewAsynchronousSession(req *ChatCompletionNewParamsWrapper) Session[ChatCompletionNewParamsWrapper] { - return &OpenAIStreamingSession{} -} -func (p *OpenAIProvider) NewSynchronousSession(req *ChatCompletionNewParamsWrapper) Session[ChatCompletionNewParamsWrapper] { - panic("not implemented") - -} - -func newOpenAIClient(baseURL, key string) openai.Client { - var opts []option.RequestOption - if key == "" { - key = os.Getenv("OPENAI_API_KEY") - } - opts = append(opts, option.WithAPIKey(key)) - if baseURL != "" { - opts = append(opts, option.WithBaseURL(baseURL)) - } - - opts = append(opts, option.WithMiddleware(LoggingMiddleware)) - - return openai.NewClient(opts...) -} diff --git a/aibridged/provider_openai_chat.go b/aibridged/provider_openai_chat.go new file mode 100644 index 0000000000000..9d26ffeced583 --- /dev/null +++ b/aibridged/provider_openai_chat.go @@ -0,0 +1,60 @@ +package aibridged + +import ( + "encoding/json" + "os" + + "github.com/openai/openai-go" + "github.com/openai/openai-go/option" + "golang.org/x/xerrors" +) + +var _ Provider[ChatCompletionNewParamsWrapper] = &OpenAIChatProvider{} + +// OpenAIChatProvider allows for interactions with the Chat Completions API. +// See https://platform.openai.com/docs/api-reference/chat. +type OpenAIChatProvider struct { + baseURL, key string +} + +func NewOpenAIChatProvider(baseURL, key string) *OpenAIChatProvider { + return &OpenAIChatProvider{ + baseURL: baseURL, + key: key, + } +} + +func (*OpenAIChatProvider) ParseRequest(payload []byte) (*ChatCompletionNewParamsWrapper, error) { + var in ChatCompletionNewParamsWrapper + if err := json.Unmarshal(payload, &in); err != nil { + return nil, xerrors.Errorf("failed to unmarshal request: %w", err) + } + + return &in, nil +} + +// NewStreamingSession creates a new session which handles streaming chat completions. +// See https://platform.openai.com/docs/api-reference/chat-streaming +func (*OpenAIChatProvider) NewStreamingSession(req *ChatCompletionNewParamsWrapper) Session { + return NewOpenAIStreamingChatSession(req) +} + +// NewBlockingSession creates a new session which handles non-streaming chat completions. +// See https://platform.openai.com/docs/api-reference/chat +func (*OpenAIChatProvider) NewBlockingSession(req *ChatCompletionNewParamsWrapper) Session { + return NewOpenAIBlockingChatSession(req) + +} + +func newOpenAIClient(baseURL, key string) openai.Client { + var opts []option.RequestOption + if key == "" { + key = os.Getenv("OPENAI_API_KEY") + } + opts = append(opts, option.WithAPIKey(key)) + if baseURL != "" { + opts = append(opts, option.WithBaseURL(baseURL)) + } + + return openai.NewClient(opts...) +} diff --git a/aibridged/session.go b/aibridged/session.go index 30c4e1155aaf8..799a6bc14850e 100644 --- a/aibridged/session.go +++ b/aibridged/session.go @@ -10,10 +10,11 @@ type Model struct { Provider, ModelName string } -type Session[Req any] interface { +// Session describes a (potentially) stateful interaction with an AI provider. +type Session interface { Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) (id string) - LastUserPrompt(req Req) (*string, error) - Model(req *Req) Model - Execute(req *Req, w http.ResponseWriter, r *http.Request) error + LastUserPrompt() (*string, error) + Model() Model + ProcessRequest(w http.ResponseWriter, r *http.Request) error Close() error } diff --git a/aibridged/session_openai.go b/aibridged/session_openai.go deleted file mode 100644 index 2eea2e335df09..0000000000000 --- a/aibridged/session_openai.go +++ /dev/null @@ -1,289 +0,0 @@ -package aibridged - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "strings" - "sync" - - "github.com/google/uuid" - "github.com/openai/openai-go" - "github.com/openai/openai-go/packages/ssestream" - "golang.org/x/xerrors" - - "cdr.dev/slog" -) - -var _ Session[ChatCompletionNewParamsWrapper] = &OpenAIStreamingSession{} - -type OpenAISessionBase struct { - id string - baseURL, key string - logger slog.Logger - tracker Tracker - toolMgr ToolManager -} - -func (s *OpenAISessionBase) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { - s.id = uuid.NewString() - - s.logger = logger.With(slog.F("session_id", s.id)) - - s.baseURL = baseURL - s.key = key - - s.tracker = tracker - s.toolMgr = toolMgr - - return s.id -} - -func (*OpenAISessionBase) LastUserPrompt(req ChatCompletionNewParamsWrapper) (*string, error) { - return req.LastUserPrompt() -} - -type OpenAIStreamingSession struct { - OpenAISessionBase -} - -func (s *OpenAIStreamingSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { - return s.OpenAISessionBase.Init(logger.Named("openai.streaming"), baseURL, key, tracker, toolMgr) -} - -func (s *OpenAIStreamingSession) Execute(req *ChatCompletionNewParamsWrapper, w http.ResponseWriter, r *http.Request) error { - if req == nil { - return xerrors.Errorf("developer error: req is nil") - } - - // Include token usage. - req.StreamOptions.IncludeUsage = openai.Bool(true) - - // Inject tools. - for _, tool := range s.toolMgr.ListTools() { - fn := openai.ChatCompletionToolParam{ - Function: openai.FunctionDefinitionParam{ - Name: tool.ID, - Strict: openai.Bool(false), // TODO: configurable. - Description: openai.String(tool.Description), - Parameters: openai.FunctionParameters{ - "type": "object", - "properties": tool.Params, - // "additionalProperties": false, // Only relevant when strict=true. - }, - }, - } - - // Otherwise the request fails with "None is not of type 'array'" if a nil slice is given. - if len(tool.Required) > 0 { - // Must list ALL properties when strict=true. - fn.Function.Parameters["required"] = tool.Required - } - - req.Tools = append(req.Tools, fn) - } - - // Allow us to interrupt watch via cancel. - ctx, cancel := context.WithCancel(r.Context()) - defer cancel() - r = r.WithContext(ctx) // Rewire context for SSE cancellation. - - client := newOpenAIClient(s.baseURL, s.key) - - streamCtx, streamCancel := context.WithCancelCause(ctx) - defer streamCancel(xerrors.New("deferred")) - - events := newEventStream(openAIEventStream) - - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - defer func() { - if err := events.Close(streamCtx); err != nil { - s.logger.Error(ctx, "error closing stream", slog.Error(err)) - } - }() - - BasicSSESender(streamCtx, events, s.logger.Named("sse-sender")).ServeHTTP(w, r) - }() - - // TODO: implement parallel tool calls. - // TODO: don't send if not supported by model (i.e. o4-mini). - if len(req.Tools) > 0 { // If no tools are specified but this setting is set, it'll cause a 400 Bad Request. - req.ParallelToolCalls = openai.Bool(false) - } - - var ( - stream *ssestream.Stream[openai.ChatCompletionChunk] - cumulativeUsage openai.CompletionUsage - ) - for { - var pendingToolCalls []openai.FinishedChatCompletionToolCall - - stream = client.Chat.Completions.NewStreaming(ctx, req.ChatCompletionNewParams) - var acc openai.ChatCompletionAccumulator - for stream.Next() { - chunk := stream.Current() - acc.AddChunk(chunk) - - // fmt.Printf("[in]: %s\n", chunk.RawJSON()) - - shouldRelayChunk := true - if toolCall, ok := acc.JustFinishedToolCall(); ok { - // Don't intercept and handle builtin tools. - if s.toolMgr.GetTool(toolCall.Name) != nil { - pendingToolCalls = append(pendingToolCalls, toolCall) - // Don't relay this chunk back; we'll handle it transparently. - shouldRelayChunk = false - } else { - s.tracker.TrackToolUsage(ctx, s.id, chunk.ID, s.Model(req), toolCall.Name, toolCall.Arguments, false, nil) - } - } - - if len(pendingToolCalls) > 0 { - // Any chunks following a tool call invocation should not be relayed. - shouldRelayChunk = false - } - - cumulativeUsage = sumUsage(cumulativeUsage, chunk.Usage) - - if shouldRelayChunk { - // If usage information is available, relay the cumulative usage once all tool invocations have completed. - if chunk.Usage.CompletionTokens > 0 { - chunk.Usage = cumulativeUsage - } - - // Overwrite response identifier since proxy obscures injected tool call invocations. - chunk.ID = s.id - events.TrySend(ctx, chunk) - } - } - - // If the usage information is set, track it. - // The API will send usage information when the response terminates, which will happen if a tool call is invoked. - s.tracker.TrackTokensUsage(ctx, s.id, acc.ID, s.Model(req), cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ - "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, - "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, - "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, - "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, - "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, - "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, - }) - - if err := stream.Err(); err != nil { - s.logger.Error(ctx, "server stream error", slog.Error(err)) - var apierr *openai.Error - if errors.As(err, &apierr) { - events.TrySend(ctx, map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - }) - // http.Error(w, apierr.Message, apierr.StatusCode) - break - } else if isConnectionError(err) { - s.logger.Warn(ctx, "upstream connection error", slog.Error(err)) - } - - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - if len(pendingToolCalls) == 0 { - break - } - - appendedPrevMsg := false - for _, tc := range pendingToolCalls { - tool := s.toolMgr.GetTool(tc.Name) - if tool == nil { - // Not an known tool, don't do anything. - s.logger.Warn(ctx, "pending tool call for non-managed tool, skipping...", slog.F("tool", tc.Name)) - continue - } - - // Only do this once. - if !appendedPrevMsg { - // Append the whole message from this stream as context since we'll be sending a new request with the tool results. - req.Messages = append(req.Messages, acc.Choices[len(acc.Choices)-1].Message.ToParam()) - appendedPrevMsg = true - } - - s.tracker.TrackToolUsage(ctx, s.id, acc.ID, s.Model(req), tc.Name, tc.Arguments, true, nil) - - var args map[string]any - if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil { - s.logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", tc.Name)) - } - - res, err := tool.Call(streamCtx, args) - if err != nil { - // Always provide a tool_result even if the tool call failed - errorResponse := map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - } - errorJSON, _ := json.Marshal(errorResponse) - req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - var out strings.Builder - if err := json.NewEncoder(&out).Encode(res); err != nil { - s.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) - // Always provide a tool_result even if encoding failed - // TODO: abstract. - errorResponse := map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - } - errorJSON, _ := json.Marshal(errorResponse) - req.Messages = append(req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - req.Messages = append(req.Messages, openai.ToolMessage(out.String(), tc.ID)) - } - } - - err := events.Close(streamCtx) - if err != nil { - s.logger.Error(ctx, "failed to close event stream", slog.Error(err)) - } - - wg.Wait() - - // Ensure we flush all the remaining data before ending. - flush(w) - - if err != nil { - streamCancel(xerrors.Errorf("stream err: %w", err)) - } else { - streamCancel(xerrors.New("gracefully done")) - } - - <-streamCtx.Done() - return nil -} - -func (s *OpenAIStreamingSession) Model(req *ChatCompletionNewParamsWrapper) Model { - var model string - if req == nil { - model = "?" - } else { - model = req.Model - } - - return Model{ - Provider: "openai", - ModelName: model, - } -} - -func (s *OpenAIStreamingSession) Close() error { - return nil // TODO: do we even need this? -} diff --git a/aibridged/session_openai_chat_base.go b/aibridged/session_openai_chat_base.go new file mode 100644 index 0000000000000..abc0ccb1d7c68 --- /dev/null +++ b/aibridged/session_openai_chat_base.go @@ -0,0 +1,94 @@ +package aibridged + +import ( + "github.com/google/uuid" + "github.com/openai/openai-go" + "golang.org/x/xerrors" + + "cdr.dev/slog" +) + +type OpenAIChatSessionBase struct { + id string + + req *ChatCompletionNewParamsWrapper + + baseURL, key string + logger slog.Logger + + tracker Tracker + toolMgr ToolManager +} + +func (s *OpenAIChatSessionBase) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { + s.id = uuid.NewString() + + s.logger = logger.With(slog.F("session_id", s.id)) + + s.baseURL = baseURL + s.key = key + + s.tracker = tracker + s.toolMgr = toolMgr + + return s.id +} + +func (s *OpenAIChatSessionBase) LastUserPrompt() (*string, error) { + if s.req == nil { + return nil, xerrors.New("nil request") + } + + return s.req.LastUserPrompt() +} + +func (s *OpenAIChatSessionBase) Model() Model { + var model string + if s.req == nil { + model = "?" + } else { + model = s.req.Model + } + + return Model{ + Provider: "openai", + ModelName: model, + } +} + +func (s *OpenAIChatSessionBase) newErrorResponse(err error) map[string]interface{} { + return map[string]interface{}{ + "error": true, + "message": err.Error(), + } +} + +func (s *OpenAIChatSessionBase) injectTools() { + if s.req == nil { + return + } + + // Inject tools. + for _, tool := range s.toolMgr.ListTools() { + fn := openai.ChatCompletionToolParam{ + Function: openai.FunctionDefinitionParam{ + Name: tool.ID, + Strict: openai.Bool(false), // TODO: configurable. + Description: openai.String(tool.Description), + Parameters: openai.FunctionParameters{ + "type": "object", + "properties": tool.Params, + // "additionalProperties": false, // Only relevant when strict=true. + }, + }, + } + + // Otherwise the request fails with "None is not of type 'array'" if a nil slice is given. + if len(tool.Required) > 0 { + // Must list ALL properties when strict=true. + fn.Function.Parameters["required"] = tool.Required + } + + s.req.Tools = append(s.req.Tools, fn) + } +} diff --git a/aibridged/session_openai_chat_blocking.go b/aibridged/session_openai_chat_blocking.go new file mode 100644 index 0000000000000..babe6016edefd --- /dev/null +++ b/aibridged/session_openai_chat_blocking.go @@ -0,0 +1,215 @@ +package aibridged + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "strings" + + "github.com/openai/openai-go" + "golang.org/x/xerrors" + + "cdr.dev/slog" +) + +var _ Session = &OpenAIBlockingChatSession{} + +type OpenAIBlockingChatSession struct { + OpenAIChatSessionBase +} + +func NewOpenAIBlockingChatSession(req *ChatCompletionNewParamsWrapper) *OpenAIBlockingChatSession { + return &OpenAIBlockingChatSession{OpenAIChatSessionBase: OpenAIChatSessionBase{ + req: req, + }} +} + +func (s *OpenAIBlockingChatSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { + return s.OpenAIChatSessionBase.Init(logger.Named("streaming"), baseURL, key, tracker, toolMgr) +} + +func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { + if s.req == nil { + return xerrors.Errorf("developer error: req is nil") + } + + ctx := r.Context() + client := newOpenAIClient(s.baseURL, s.key) + logger := s.logger.With(slog.F("model", s.req.Model)) + + var cumulativeUsage openai.CompletionUsage + var ( + completion *openai.ChatCompletion + err error + ) + + s.injectTools() + + prompt, err := s.LastUserPrompt() + if err != nil { + logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) + } + + for { + completion, err = client.Chat.Completions.New(ctx, s.req.ChatCompletionNewParams) + if err != nil { + break + } + + if prompt != nil { + if err := s.tracker.TrackPromptUsage(ctx, s.id, completion.ID, s.Model(), *prompt, nil); err != nil { + logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) + } + } + + // Track cumulative usage + cumulativeUsage = sumUsage(cumulativeUsage, completion.Usage) + + // Track token usage + if err := s.tracker.TrackTokensUsage(ctx, s.id, completion.ID, s.Model(), cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ + "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, + "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, + "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, + "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, + "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, + "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, + }); err != nil { + logger.Warn(ctx, "failed to track tokens usage", slog.Error(err)) + } + + // Check if we have tool calls to process + var pendingToolCalls []openai.ChatCompletionMessageToolCall + if len(completion.Choices) > 0 && completion.Choices[0].Message.ToolCalls != nil { + for _, toolCall := range completion.Choices[0].Message.ToolCalls { + if s.toolMgr.GetTool(toolCall.Function.Name) != nil { + pendingToolCalls = append(pendingToolCalls, toolCall) + } else { + if err := s.tracker.TrackToolUsage(ctx, s.id, completion.ID, s.Model(), toolCall.Function.Name, toolCall.Function.Arguments, false, nil); err != nil { + s.logger.Warn(ctx, "failed to track tool usage", slog.Error(err), slog.F("tool", toolCall.Function.Name)) + } + } + } + } + + // If no injected tool calls, we're done + if len(pendingToolCalls) == 0 { + break + } + + appendedPrevMsg := false + // Process each pending tool call + for _, tc := range pendingToolCalls { + tool := s.toolMgr.GetTool(tc.Function.Name) + if tool == nil { + // Not a known tool, don't do anything. + logger.Warn(ctx, "pending tool call for non-managed tool, skipping", slog.F("tool", tc.Function.Name)) + continue + } + // Only do this once. + if !appendedPrevMsg { + // Append the whole message from this stream as context since we'll be sending a new request with the tool results. + s.req.Messages = append(s.req.Messages, completion.Choices[0].Message.ToParam()) + appendedPrevMsg = true + } + + if err := s.tracker.TrackToolUsage(ctx, s.id, completion.ID, s.Model(), tool.Name, tc.Function.Arguments, true, nil); err != nil { + logger.Warn(ctx, "failed to track tool usage", slog.Error(err), slog.F("tool", tool.Name)) + } + + var ( + args map[string]string + buf bytes.Buffer + ) + _ = json.NewEncoder(&buf).Encode(tc.Function.Arguments) + _ = json.NewDecoder(&buf).Decode(&args) + res, err := tool.Call(ctx, args) + if err != nil { + // Always provide a tool result even if the tool call failed + errorResponse := map[string]interface{}{ + // TODO: session ID? + "error": true, + "message": err.Error(), + } + errorJSON, _ := json.Marshal(errorResponse) + s.req.Messages = append(s.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + var out strings.Builder + if err := json.NewEncoder(&out).Encode(res); err != nil { + logger.Error(ctx, "failed to encode tool response", slog.Error(err)) + // Always provide a tool result even if encoding failed + errorResponse := map[string]interface{}{ + // TODO: session ID? + "error": true, + "message": err.Error(), + } + errorJSON, _ := json.Marshal(errorResponse) + s.req.Messages = append(s.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + s.req.Messages = append(s.req.Messages, openai.ToolMessage(out.String(), tc.ID)) + } + } + + // TODO: these probably have to be formatted as JSON errs? + if err != nil { + if isConnectionError(err) { + http.Error(w, err.Error(), http.StatusInternalServerError) + return xerrors.Errorf("upstream connection closed: %w", err) + } + var apierr *openai.Error + if errors.As(err, &apierr) { + http.Error(w, apierr.Message, apierr.StatusCode) + return xerrors.Errorf("api error: %w", apierr) + } + + http.Error(w, err.Error(), http.StatusInternalServerError) + return xerrors.Errorf("chat completion failed: %w", err) + } + + if completion == nil { + return nil + } + + // Overwrite response identifier since proxy obscures injected tool call invocations. + completion.ID = s.id + + // Update the cumulative usage in the final response + if completion.Usage.CompletionTokens > 0 { + completion.Usage = cumulativeUsage + } + + w.Header().Set("Content-Type", "application/json") + out, err := json.Marshal(completion) + if err != nil { + out, _ = json.Marshal(s.newErrorResponse(xerrors.Errorf("failed to marshal response: %w", err))) + w.WriteHeader(http.StatusInternalServerError) + } else { + w.WriteHeader(http.StatusOK) + } + + _, _ = w.Write(out) + + return nil +} + +func (s *OpenAIBlockingChatSession) Model() Model { + var model string + if s.req == nil { + model = "?" + } else { + model = s.req.Model + } + + return Model{ + Provider: "openai", + ModelName: model, + } +} + +func (s *OpenAIBlockingChatSession) Close() error { + return nil // TODO: do we even need this? +} diff --git a/aibridged/session_openai_chat_streaming.go b/aibridged/session_openai_chat_streaming.go new file mode 100644 index 0000000000000..cf26901935ce2 --- /dev/null +++ b/aibridged/session_openai_chat_streaming.go @@ -0,0 +1,239 @@ +package aibridged + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "strings" + "sync" + + "github.com/openai/openai-go" + "github.com/openai/openai-go/packages/ssestream" + "golang.org/x/xerrors" + + "cdr.dev/slog" +) + +var _ Session = &OpenAIStreamingChatSession{} + +type OpenAIStreamingChatSession struct { + OpenAIChatSessionBase +} + +func NewOpenAIStreamingChatSession(req *ChatCompletionNewParamsWrapper) *OpenAIStreamingChatSession { + return &OpenAIStreamingChatSession{OpenAIChatSessionBase: OpenAIChatSessionBase{ + req: req, + }} +} + +func (s *OpenAIStreamingChatSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { + return s.OpenAIChatSessionBase.Init(logger.Named("streaming"), baseURL, key, tracker, toolMgr) +} + +func (s *OpenAIStreamingChatSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { + if s.req == nil { + return xerrors.Errorf("developer error: req is nil") + } + + // Include token usage. + s.req.StreamOptions.IncludeUsage = openai.Bool(true) + + s.injectTools() + + // Allow us to interrupt watch via cancel. + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + r = r.WithContext(ctx) // Rewire context for SSE cancellation. + + client := newOpenAIClient(s.baseURL, s.key) + logger := s.logger.With(slog.F("model", s.req.Model)) + + streamCtx, streamCancel := context.WithCancelCause(ctx) + defer streamCancel(xerrors.New("deferred")) + + events := newEventStream(openAIEventStream) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + defer func() { + if err := events.Close(streamCtx); err != nil { + logger.Error(ctx, "error closing stream", slog.Error(err)) + } + }() + + BasicSSESender(streamCtx, events, logger.Named("sse-sender")).ServeHTTP(w, r) + }() + + // TODO: implement parallel tool calls. + // TODO: don't send if not supported by model (i.e. o4-mini). + if len(s.req.Tools) > 0 { // If no tools are specified but this setting is set, it'll cause a 400 Bad Request. + s.req.ParallelToolCalls = openai.Bool(false) + } + + prompt, err := s.LastUserPrompt() + if err != nil { + logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) + } + + var ( + stream *ssestream.Stream[openai.ChatCompletionChunk] + cumulativeUsage openai.CompletionUsage + ) + for { + var pendingToolCalls []openai.FinishedChatCompletionToolCall + + stream = client.Chat.Completions.NewStreaming(ctx, s.req.ChatCompletionNewParams) + var acc openai.ChatCompletionAccumulator + for stream.Next() { + chunk := stream.Current() + acc.AddChunk(chunk) + + shouldRelayChunk := true + if toolCall, ok := acc.JustFinishedToolCall(); ok { + // Don't intercept and handle builtin tools. + if s.toolMgr.GetTool(toolCall.Name) != nil { + pendingToolCalls = append(pendingToolCalls, toolCall) + // Don't relay this chunk back; we'll handle it transparently. + shouldRelayChunk = false + } else { + if err := s.tracker.TrackToolUsage(ctx, s.id, chunk.ID, s.Model(), toolCall.Name, toolCall.Arguments, false, nil); err != nil { + logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) + } + } + } + + if len(pendingToolCalls) > 0 { + // Any chunks following a tool call invocation should not be relayed. + shouldRelayChunk = false + } + + cumulativeUsage = sumUsage(cumulativeUsage, chunk.Usage) + + if shouldRelayChunk { + // If usage information is available, relay the cumulative usage once all tool invocations have completed. + if chunk.Usage.CompletionTokens > 0 { + chunk.Usage = cumulativeUsage + } + + // Overwrite response identifier since proxy obscures injected tool call invocations. + chunk.ID = s.id + events.TrySend(ctx, chunk) + } + } + + if prompt != nil { + if err := s.tracker.TrackPromptUsage(ctx, s.id, acc.ID, s.Model(), *prompt, nil); err != nil { + logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) + } + } + + // If the usage information is set, track it. + // The API will send usage information when the response terminates, which will happen if a tool call is invoked. + if err := s.tracker.TrackTokensUsage(ctx, s.id, acc.ID, s.Model(), cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ + "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, + "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, + "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, + "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, + "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, + "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, + }); err != nil { + logger.Warn(ctx, "failed to track tokens usage", slog.Error(err)) + } + + if err := stream.Err(); err != nil { + logger.Error(ctx, "server stream error", slog.Error(err)) + var apierr *openai.Error + if errors.As(err, &apierr) { + events.TrySend(ctx, s.newErrorResponse(err)) + break + } else if isConnectionError(err) { + logger.Warn(ctx, "upstream connection error", slog.Error(err)) + } + + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if len(pendingToolCalls) == 0 { + break + } + + appendedPrevMsg := false + for _, tc := range pendingToolCalls { + tool := s.toolMgr.GetTool(tc.Name) + if tool == nil { + // Not a known tool, don't do anything. + logger.Warn(ctx, "pending tool call for non-managed tool, skipping", slog.F("tool", tc.Name)) + continue + } + + // Only do this once. + if !appendedPrevMsg { + // Append the whole message from this stream as context since we'll be sending a new request with the tool results. + s.req.Messages = append(s.req.Messages, acc.Choices[len(acc.Choices)-1].Message.ToParam()) + appendedPrevMsg = true + } + + var toolName string + _, toolName, err = DecodeToolID(tc.Name) + if err != nil { + logger.Debug(ctx, "failed to decode tool ID", slog.Error(err), slog.F("name", tc.Name)) + toolName = tc.Name + } + + if err := s.tracker.TrackToolUsage(ctx, s.id, acc.ID, s.Model(), toolName, tc.Arguments, true, nil); err != nil { + logger.Warn(ctx, "failed to track tool usage", slog.Error(err), slog.F("tool", tool.Name)) + } + + var args map[string]any + if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil { + logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", toolName)) + } + + res, err := tool.Call(streamCtx, args) + if err != nil { + // Always provide a tool_result even if the tool call failed. + errorJSON, _ := json.Marshal(s.newErrorResponse(err)) + s.req.Messages = append(s.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + var out strings.Builder + if err := json.NewEncoder(&out).Encode(res); err != nil { + logger.Error(ctx, "failed to encode tool response", slog.Error(err)) + // Always provide a tool_result even if encoding failed. + errorJSON, _ := json.Marshal(s.newErrorResponse(err)) + s.req.Messages = append(s.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) + continue + } + + s.req.Messages = append(s.req.Messages, openai.ToolMessage(out.String(), tc.ID)) + } + } + + err = events.Close(streamCtx) + if err != nil { + logger.Error(ctx, "failed to close event stream", slog.Error(err)) + } + + wg.Wait() + + // Ensure we flush all the remaining data before ending. + flush(w) + + if err != nil { + streamCancel(xerrors.Errorf("stream err: %w", err)) + } else { + streamCancel(xerrors.New("gracefully done")) + } + + <-streamCtx.Done() + return nil +} + +func (s *OpenAIStreamingChatSession) Close() error { + return nil // TODO: do we even need this? +} diff --git a/aibridged/tracking.go b/aibridged/tracking.go deleted file mode 100644 index 00e7056929a4a..0000000000000 --- a/aibridged/tracking.go +++ /dev/null @@ -1,80 +0,0 @@ -package aibridged - -import ( - "regexp" - "strings" - - "github.com/anthropics/anthropic-sdk-go" - "golang.org/x/xerrors" - "tailscale.com/types/ptr" -) - -// -// -// -// -// TODO: decompose this file into more appropriate locations. -// -// -// -// - -type UsageExtractor interface { - LastUserPrompt() (*string, error) - LastToolCalls() ([]string, error) -} - -func (b *BetaMessageNewParamsWrapper) LastUserPrompt() (*string, error) { - if b == nil { - return nil, xerrors.New("nil struct") - } - - if len(b.Messages) == 0 { - return nil, xerrors.New("no messages") - } - - var userMessage string - for i := len(b.Messages) - 1; i >= 0; i-- { - m := b.Messages[i] - if m.Role != anthropic.BetaMessageParamRoleUser { - continue - } - if len(m.Content) == 0 { - continue - } - - for j := len(m.Content) - 1; j >= 0; j-- { - if textContent := m.Content[j].GetText(); textContent != nil { - userMessage = *textContent - } - - // Ignore internal Claude Code prompts. - if userMessage == "test" || - strings.Contains(userMessage, "") { - userMessage = "" - continue - } - - // Handle Cursor-specific formatting by extracting content from tags - if isCursor, _ := regexp.MatchString("", userMessage); isCursor { - userMessage = extractCursorUserQuery(userMessage) - } - return ptr.To(strings.TrimSpace(userMessage)), nil - } - } - - return nil, nil -} - -func extractCursorUserQuery(message string) string { - pat := regexp.MustCompile(`(?P[\s\S]*?)`) - match := pat.FindStringSubmatch(message) - if match != nil { - // Get the named group by index - contentIndex := pat.SubexpIndex("content") - if contentIndex != -1 { - message = match[contentIndex] - } - } - return strings.TrimSpace(message) -} diff --git a/coderd/aibridge.go b/coderd/aibridge.go index 2f9653ec20f10..61fdd76ed998f 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -65,7 +65,7 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { http.StripPrefix("/api/v2/aibridge", bridge.Handler()).ServeHTTP(rw, r) } -func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, clientFn func() (aibridgedproto.DRPCAIBridgeDaemonClient, bool)) (*aibridged.Bridge, error) { +func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, clientFn func() (aibridgedproto.DRPCAIBridgeDaemonClient, error)) (*aibridged.Bridge, error) { if api.AIBridges == nil { return nil, xerrors.New("bridge cache storage is not configured") } From acd0f2870bb9fbdc523d39a187bd3362c20ffdd6 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 7 Aug 2025 18:01:05 +0200 Subject: [PATCH 53/61] refactor anthropic Signed-off-by: Danny Kopping --- aibridged/anthropic_utils.go | 58 ++ aibridged/bridge.go | 903 ++---------------- aibridged/provider_anthropic_messages.go | 41 + aibridged/session_anthropic_messages_base.go | 95 ++ .../session_anthropic_messages_blocking.go | 278 ++++++ .../session_anthropic_messages_streaming.go | 386 ++++++++ 6 files changed, 922 insertions(+), 839 deletions(-) create mode 100644 aibridged/anthropic_utils.go create mode 100644 aibridged/provider_anthropic_messages.go create mode 100644 aibridged/session_anthropic_messages_base.go create mode 100644 aibridged/session_anthropic_messages_blocking.go create mode 100644 aibridged/session_anthropic_messages_streaming.go diff --git a/aibridged/anthropic_utils.go b/aibridged/anthropic_utils.go new file mode 100644 index 0000000000000..3802d8f1d225f --- /dev/null +++ b/aibridged/anthropic_utils.go @@ -0,0 +1,58 @@ +package aibridged + +import ( + "encoding/json" + "errors" + "os" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" +) + +// newAnthropicClient creates an Anthropic client with the given base URL and API key. +func newAnthropicClient(baseURL, key string, opts ...option.RequestOption) anthropic.Client { + if key == "" { + key = os.Getenv("ANTHROPIC_API_KEY") + } + opts = append(opts, option.WithAPIKey(key)) + if baseURL != "" { + opts = append(opts, option.WithBaseURL(baseURL)) + } + + return anthropic.NewClient(opts...) +} + +func getAnthropicErrorResponse(err error) *AnthropicErrorResponse { + var apierr *anthropic.Error + if !errors.As(err, &apierr) { + return nil + } + + msg := apierr.Error() + + var detail *anthropic.BetaAPIError + if field, ok := apierr.JSON.ExtraFields["error"]; ok { + _ = json.Unmarshal([]byte(field.Raw()), &detail) + } + if detail != nil { + msg = detail.Message + } + + return &AnthropicErrorResponse{ + BetaErrorResponse: &anthropic.BetaErrorResponse{ + Error: anthropic.BetaErrorUnion{ + Message: msg, + Type: string(detail.Type), + }, + Type: ant_constant.ValueOf[ant_constant.Error](), + }, + StatusCode: apierr.StatusCode, + } +} + +type AnthropicErrorResponse struct { + *anthropic.BetaErrorResponse + + StatusCode int `json:"-"` +} diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 34cd08420ddbb..bf610c4b4a06a 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -2,23 +2,11 @@ package aibridged import ( "context" - "encoding/json" - "errors" "fmt" "io" "net/http" - "net/url" - "os" - "strings" - "sync" "time" - "github.com/anthropics/anthropic-sdk-go" - "github.com/anthropics/anthropic-sdk-go/option" - ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" - "github.com/google/uuid" - "github.com/mark3labs/mcp-go/mcp" - "github.com/openai/openai-go/shared/constant" "golang.org/x/xerrors" "cdr.dev/slog" @@ -27,33 +15,6 @@ import ( "github.com/coder/coder/v2/codersdk" ) -// Error type constants for structured error reporting -const ( - ErrorTypeRequestCanceled = "request_canceled" - ErrorTypeConnectionError = "connection_error" - ErrorTypeUnexpectedError = "unexpected_error" - ErrorTypeAnthropicAPIError = "anthropic_api_error" - ErrorTypeOpenAIAPIError = "openai_api_error" - ErrorTypeInternalError = "internal_error" - ErrorTypeValidationError = "validation_error" - ErrorTypeAuthenticationError = "authentication_error" - ErrorTypeRateLimitError = "rate_limit_error" - ErrorTypeTimeoutError = "timeout_error" -) - -// BridgeError represents a structured error from the bridge that can carry -// specific error information back to the client. -type BridgeError struct { - Code string `json:"code"` - Message string `json:"message"` - StatusCode int `json:"status_code"` - Details map[string]string `json:"details,omitempty"` -} - -func (e *BridgeError) Error() string { - return e.Message -} - // Bridge is responsible for proxying requests to upstream AI providers. // // Characteristics: @@ -76,54 +37,11 @@ type Bridge struct { tools map[string]*MCPTool } -func handleOpenAI(provider *OpenAIChatProvider, drpcClient proto.DRPCAIBridgeDaemonClient, tools map[string][]*MCPTool, logger slog.Logger) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // Read and parse request. - body, err := io.ReadAll(r.Body) - if err != nil { - if isConnectionError(err) { - logger.Debug(r.Context(), "client disconnected during request body read", slog.Error(err)) - return // Don't send error response if client already disconnected - } - logger.Error(r.Context(), "failed to read body", slog.Error(err)) - http.Error(w, "failed to read body", http.StatusInternalServerError) - return - } - req, err := provider.ParseRequest(body) - if err != nil { - logger.Error(r.Context(), "failed to parse request", slog.Error(err)) - http.Error(w, "failed to parse request", http.StatusBadRequest) - return - } - - // Create a new session. - var sess Session - if req.Stream { - sess = provider.NewStreamingSession(req) - } else { - sess = provider.NewBlockingSession(req) - } - - sessID := sess.Init(logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) - logger.Debug(context.Background(), "starting openai session", slog.F("session_id", sessID)) - - defer func() { - if err := sess.Close(); err != nil { - logger.Warn(context.Background(), "failed to close session", slog.Error(err), slog.F("session_id", sessID), slog.F("kind", fmt.Sprintf("%T", sess))) - } - }() - - // Process the request. - if err := sess.ProcessRequest(w, r); err != nil { - logger.Error(r.Context(), "session execution failed", slog.Error(err)) - } - } -} - func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, error), tools map[string][]*MCPTool) (*Bridge, error) { var bridge Bridge - openAIProvider := NewOpenAIChatProvider(cfg.OpenAI.BaseURL.String(), cfg.OpenAI.Key.String()) + openAIChatProvider := NewOpenAIChatProvider(cfg.OpenAI.BaseURL.String(), cfg.OpenAI.Key.String()) + anthropicMessagesProvider := NewAnthropicMessagesProvider(cfg.Anthropic.BaseURL.String(), cfg.Anthropic.Key.String()) drpcClient, err := clientFn() if err != nil { @@ -131,8 +49,8 @@ func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() } mux := &http.ServeMux{} - mux.HandleFunc("/v1/chat/completions", handleOpenAI(openAIProvider, drpcClient, tools, logger.Named("openai"))) - mux.HandleFunc("/v1/messages", bridge.proxyAnthropicRequest) + mux.HandleFunc("/v1/chat/completions", handleOpenAIChat(openAIChatProvider, drpcClient, tools, logger.Named("openai"))) + mux.HandleFunc("/v1/messages", handleAnthropicMessages(anthropicMessagesProvider, drpcClient, tools, logger.Named("anthropic"))) srv := &http.Server{ Handler: mux, @@ -159,787 +77,94 @@ func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() return &bridge, nil } -func (b *Bridge) openAITarget() *url.URL { - u := b.cfg.OpenAI.BaseURL.String() - target, err := url.Parse(u) - if err != nil { - panic(fmt.Sprintf("failed to parse %q", u)) - } - return target -} - func (b *Bridge) Handler() http.Handler { return b.httpSrv.Handler } -// TODO: track cumulative usage when tool invocations are executed; see OpenAI implementation. -func (b *Bridge) proxyAnthropicRequest(w http.ResponseWriter, r *http.Request) { - sessionID := uuid.NewString() - b.logger.Info(r.Context(), "anthropic request started", slog.F("session_id", sessionID), slog.F("method", r.Method), slog.F("path", r.URL.Path)) - _, _ = fmt.Fprintf(os.Stderr, "[%s] new chat session started\n\n", sessionID) - - defer func() { - b.logger.Info(r.Context(), "anthropic request ended", slog.F("session_id", sessionID)) - _, _ = fmt.Fprintf(os.Stderr, "[%s] chat session ended\n\n", sessionID) - }() - - // Allow us to interrupt watch via cancel. - ctx, cancel := context.WithCancel(r.Context()) - defer cancel() - r = r.WithContext(ctx) // Rewire context for SSE cancellation. - - useBeta := r.URL.Query().Get("beta") == "true" - if !useBeta { - b.logger.Warn(r.Context(), "non-beta API requested, using beta instead", slog.F("url", r.URL.String())) - useBeta = true - // http.Error(w, "only beta API supported", http.StatusInternalServerError) - // return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - b.logger.Error(r.Context(), "failed to read body", slog.Error(err)) - http.Error(w, "failed to read body", http.StatusInternalServerError) - return - } - - // var in streamer - // if useBeta { - var in BetaMessageNewParamsWrapper - // } else { - // in = &MessageNewParamsWrapper{} - //} - - if err = json.Unmarshal(body, &in); err != nil { - b.logger.Error(r.Context(), "failed to unmarshal request", slog.Error(err)) - http.Error(w, "failed to unmarshal request", http.StatusInternalServerError) - return - } - - // Policy examples. - if strings.Contains(string(in.Model), "opus") { - err := xerrors.Errorf("%q model is not allowed", in.Model) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - for _, m := range in.Messages { - for _, c := range m.Content { - if c.OfText == nil { - continue - } - - if strings.Contains(c.OfText.Text, ".env") { - http.Error(w, "Request blocked due to attempted access to sensitive file; this has been logged.", http.StatusBadRequest) - return - } - } - } - - for _, t := range in.Tools { - if t.OfTool == nil { - continue - } - - if strings.Contains(t.OfTool.Name, "mcp__") && (!strings.Contains(t.OfTool.Name, "go") && !strings.Contains(t.OfTool.Name, "typescript")) { - segs := strings.Split(t.OfTool.Name, "__") - var serverName string - if len(segs) >= 1 { - serverName = segs[1] +func handleOpenAIChat(provider *OpenAIChatProvider, drpcClient proto.DRPCAIBridgeDaemonClient, tools map[string][]*MCPTool, logger slog.Logger) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Read and parse request. + body, err := io.ReadAll(r.Body) + if err != nil { + if isConnectionError(err) { + logger.Debug(r.Context(), "client disconnected during request body read", slog.Error(err)) + return // Don't send error response if client already disconnected } - - http.Error(w, fmt.Sprintf("Request blocked due to MCP server %q being used; this has been logged.", serverName), http.StatusBadRequest) + logger.Error(r.Context(), "failed to read body", slog.Error(err)) + http.Error(w, "failed to read body", http.StatusInternalServerError) return } - } - - for _, tool := range b.tools { - in.Tools = append(in.Tools, anthropic.BetaToolUnionParam{ - OfTool: &anthropic.BetaToolParam{ - InputSchema: anthropic.BetaToolInputSchemaParam{ - Properties: tool.Params, - Required: tool.Required, - }, - Name: tool.ID, - Description: anthropic.String(tool.Description), - Type: anthropic.BetaToolTypeCustom, - }, - }) - } - - // Claude Code uses the 3.5 Haiku model to do autocomplete and other small tasks. (see ANTHROPIC_SMALL_FAST_MODEL). - // It's highly unlikely that operators want to see these prompts tracked, but the token usage must be. - // We could consider making this configurable in the future. - isSmallFastModel := strings.Contains(string(in.Model), "3-5-haiku") - - // Find the most recent user message and track the prompt. - if !isSmallFastModel { - prompt, _ := in.LastUserPrompt() // TODO: error handling. - if prompt != nil { - b.trackUserPrompt(ctx, sessionID, "", string(in.Model), *prompt) + req, err := provider.ParseRequest(body) + if err != nil { + logger.Error(r.Context(), "failed to parse request", slog.Error(err)) + http.Error(w, "failed to parse request", http.StatusBadRequest) + return } - } - messages := in.BetaMessageNewParams - - // Note: Parallel tool calls are disabled in the processing loop to avoid tool_use/tool_result block mismatches - messages.ToolChoice = anthropic.BetaToolChoiceUnionParam{ - OfAny: &anthropic.BetaToolChoiceAnyParam{ - Type: "auto", - DisableParallelToolUse: anthropic.Bool(true), - }, - } - - var opts []option.RequestOption - if reqBetaHeader := r.Header.Get("anthropic-beta"); strings.TrimSpace(reqBetaHeader) != "" { - opts = append(opts, option.WithHeader("anthropic-beta", reqBetaHeader)) - } - // opts = append(opts, option.WithMiddleware(LoggingMiddleware)) - - apiKey := b.cfg.Anthropic.Key.String() - if apiKey == "" { - apiKey = os.Getenv("ANTHROPIC_API_KEY") - } - - opts = append(opts, option.WithAPIKey(apiKey)) - opts = append(opts, option.WithBaseURL(b.cfg.Anthropic.BaseURL.String())) - - // looks up API key with os.LookupEnv("ANTHROPIC_API_KEY") - client := anthropic.NewClient(opts...) - if !in.UseStreaming() { - opts = append(opts, option.WithRequestTimeout(time.Second*30)) // TODO: configurable. - for { - resp, err := client.Beta.Messages.New(ctx, messages, opts...) - if err != nil { - if isConnectionError(err) { - b.logger.Warn(ctx, "upstream connection closed", slog.Error(err)) - return - } - - b.logger.Error(ctx, "anthropic stream error", slog.Error(err)) - if antErr := getAnthropicErrorResponse(err); antErr != nil { - http.Error(w, antErr.Error.Message, antErr.StatusCode) - return - } - - b.logger.Error(ctx, "upstream API error", slog.Error(err)) - http.Error(w, "internal error", http.StatusInternalServerError) - return - } - - b.trackTokenUsage(ctx, sessionID, resp.ID, string(resp.Model), resp.Usage.InputTokens, resp.Usage.OutputTokens, map[string]int64{ - "web_search_requests": resp.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": resp.Usage.CacheCreationInputTokens, - "cache_read_input": resp.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": resp.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": resp.Usage.CacheCreation.Ephemeral5mInputTokens, - }) - - // Handle tool calls for non-streaming. - var pendingToolCalls []anthropic.BetaToolUseBlock - for _, c := range resp.Content { - toolUse := c.AsToolUse() - if toolUse.ID == "" { - continue - } - - if b.isInjectedTool(toolUse.Name) { - pendingToolCalls = append(pendingToolCalls, toolUse) - continue - } - - // If tool is not injected, track it since the client will be handling it. - b.trackToolUsage(ctx, sessionID, resp.ID, string(resp.Model), toolUse.Name, toolUse.Input, false) - } - - // If no injected tool calls, we're done. - if len(pendingToolCalls) == 0 { - // Overwrite response identifier since proxy obscures injected tool call invocations. - resp.ID = sessionID - - out, err := json.Marshal(resp) - if err != nil { - http.Error(w, "error marshaling response", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(out) - break - } - - // Append the assistant's message (which contains the tool_use block) - // to the messages for the next API call. - messages.Messages = append(messages.Messages, resp.ToParam()) - - // Process each pending tool call. - for _, tc := range pendingToolCalls { - tool := b.tools[tc.Name] - - var args map[string]any - serialized, err := json.Marshal(tc.Input) - if err != nil { - b.logger.Warn(ctx, "failed to marshal tool args for unmarshal", slog.Error(err), slog.F("tool", tc.Name)) - // Continue to next tool call, but still append an error tool_result - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error unmarshaling tool arguments: %v", err), true)), - ) - continue - } else if err := json.Unmarshal(serialized, &args); err != nil { - b.logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", tc.Name)) - // Continue to next tool call, but still append an error tool_result - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error unmarshaling tool arguments: %v", err), true)), - ) - continue - } - - b.trackToolUsage(ctx, sessionID, resp.ID, string(resp.Model), tc.Name, args, true) - - res, err := tool.Call(ctx, args) - if err != nil { - // Always provide a tool_result even if the tool call failed - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error calling tool: %v", err), true)), - ) - continue - } - - // Ensure at least one tool_result is always added for each tool_use. - toolResult := anthropic.BetaContentBlockParamUnion{ - OfToolResult: &anthropic.BetaToolResultBlockParam{ - ToolUseID: tc.ID, - IsError: anthropic.Bool(false), - }, - } - - var hasValidResult bool - for _, content := range res.Content { - switch cb := content.(type) { - case mcp.TextContent: - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: cb.Text, - }, - }) - hasValidResult = true - case mcp.EmbeddedResource: - switch resource := cb.Resource.(type) { - case mcp.TextResourceContents: - val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", - resource.MIMEType, resource.URI, resource.Text) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: val, - }, - }) - hasValidResult = true - case mcp.BlobResourceContents: - val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", - resource.MIMEType, resource.URI, resource.Blob) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: val, - }, - }) - hasValidResult = true - default: - b.logger.Error(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource))) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: unknown embedded resource type", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - hasValidResult = true - } - default: - b.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb))) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: unsupported tool result type", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - hasValidResult = true - } - } - - // If no content was processed, still add a tool_result - if !hasValidResult { - b.logger.Error(ctx, "no tool result added", slog.F("content_len", len(res.Content)), slog.F("is_error", res.IsError)) // This can only happen if there's somehow no content. - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: no valid tool result content", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - } - - if len(toolResult.OfToolResult.Content) > 0 { - messages.Messages = append(messages.Messages, anthropic.NewBetaUserMessage(toolResult)) - } - } + // Create a new session. + var sess Session + if req.Stream { + sess = provider.NewStreamingSession(req) + } else { + sess = provider.NewBlockingSession(req) } - return - } - - streamCtx, streamCancel := context.WithCancelCause(r.Context()) - - es := newEventStream(anthropicEventStream) - var wg sync.WaitGroup - wg.Add(1) + sessID := sess.Init(logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) + logger.Debug(context.Background(), "starting openai session", slog.F("session_id", sessID)) - go func() { - defer wg.Done() defer func() { - if err := es.Close(streamCtx); err != nil { - b.logger.Error(ctx, "error closing stream", slog.Error(err), slog.F("session_id", sessionID)) + if err := sess.Close(); err != nil { + logger.Warn(context.Background(), "failed to close session", slog.Error(err), slog.F("session_id", sessID), slog.F("kind", fmt.Sprintf("%T", sess))) } }() - BasicSSESender(streamCtx, es, b.logger.Named("sse-sender")).ServeHTTP(w, r) - }() - - isFirst := true - for { - newStream: - stream := client.Beta.Messages.NewStreaming(streamCtx, messages) - - var events []anthropic.BetaRawMessageStreamEventUnion - var message anthropic.BetaMessage - var lastToolName string - - pendingToolCalls := make(map[string]string) - - for stream.Next() { - event := stream.Current() - events = append(events, event) - - if err := message.Accumulate(event); err != nil { - b.logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) - http.Error(w, "failed to proxy request", http.StatusInternalServerError) - return // TODO: don't return, skip to close. - } - - // Tool-related handling. - switch event.Type { - case string(constant.ValueOf[ant_constant.ContentBlockStart]()): // Have to do this because otherwise content_block_delta and content_block_start both match the type anthropic.BetaRawContentBlockStartEvent - switch block := event.AsContentBlockStart().ContentBlock.AsAny().(type) { - case anthropic.BetaToolUseBlock: - lastToolName = block.Name - - if b.isInjectedTool(block.Name) { - pendingToolCalls[block.Name] = block.ID - // Don't relay this event back, otherwise the client will try invoke the tool as well. - continue - } - } - case string(constant.ValueOf[ant_constant.ContentBlockDelta]()): - if len(pendingToolCalls) > 0 && b.isInjectedTool(lastToolName) { - // We're busy with a tool call, don't relay this event back. - continue - } - case string(constant.ValueOf[ant_constant.ContentBlockStop]()): - // Reset the tool name - isInjected := b.isInjectedTool(lastToolName) - lastToolName = "" - - if len(pendingToolCalls) > 0 && isInjected { - // We're busy with a tool call, don't relay this event back. - continue - } - case string(ant_constant.ValueOf[ant_constant.MessageStart]()): - // Anthropic's docs only mention usage in message_delta events, but it's also present in message_start. - // See https://docs.anthropic.com/en/docs/build-with-claude/streaming#event-types. - start := event.AsMessageStart() - b.trackTokenUsage(streamCtx, sessionID, message.ID, string(message.Model), start.Message.Usage.InputTokens, start.Message.Usage.OutputTokens, map[string]int64{ - "web_search_requests": start.Message.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": start.Message.Usage.CacheCreationInputTokens, - "cache_read_input": start.Message.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, - }) - - if !isFirst { - // Don't send message_start unless first message! - // We're sending multiple messages back and forth with the API, but from the client's perspective - // they're just expecting a single message. - continue - } - case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): - delta := event.AsMessageDelta() - b.trackTokenUsage(streamCtx, sessionID, message.ID, string(message.Model), delta.Usage.InputTokens, delta.Usage.OutputTokens, map[string]int64{ - "web_search_requests": delta.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": delta.Usage.CacheCreationInputTokens, - "cache_read_input": delta.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, - }) - - // Don't relay message_delta events which indicate injected tool use. - if len(pendingToolCalls) > 0 && b.isInjectedTool(lastToolName) { - continue - } - - // If currently calling a tool. - if message.Content[len(message.Content)-1].Type == string(ant_constant.ValueOf[ant_constant.ToolUse]()) { - toolName := message.Content[len(message.Content)-1].AsToolUse().Name - if len(pendingToolCalls) > 0 && b.isInjectedTool(toolName) { - continue - } - } - - // Don't send message_stop until all tools have been called. - case string(ant_constant.ValueOf[ant_constant.MessageStop]()): - - if len(pendingToolCalls) > 0 { - // Append the whole message from this stream as context since we'll be sending a new request with the tool results. - messages.Messages = append(messages.Messages, message.ToParam()) - - for name, id := range pendingToolCalls { - if !b.isInjectedTool(name) { - // Not an MCP proxy call, don't do anything. - continue - } - - tool := b.tools[name] - - var ( - input any - foundTool bool - foundTools int - ) - for _, block := range message.Content { - switch variant := block.AsAny().(type) { - case anthropic.BetaToolUseBlock: - foundTools++ - if variant.Name == name { - input = variant.Input - foundTool = true - } - } - } - - if !foundTool { - b.logger.Error(ctx, "failed to find tool input", slog.F("tool_name", name), slog.F("found_tools", foundTools)) - continue - } - - b.trackToolUsage(streamCtx, sessionID, message.ID, string(message.Model), tool.Name, input, true) - - res, err := b.tools[tool.ID].Call(streamCtx, input) - if err != nil { - // Always provide a tool_result even if the tool call failed - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, fmt.Sprintf("Error calling tool: %v", err), true)), - ) - continue - } - - var out strings.Builder - if err := json.NewEncoder(&out).Encode(res); err != nil { - b.logger.Error(ctx, "failed to encode tool response", slog.Error(err)) - // Always provide a tool_result even if encoding failed - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, fmt.Sprintf("Error encoding tool response: %v", err), true)), - ) - continue - } - - // Ensure at least one tool_result is always added for each tool_use - toolResult := anthropic.BetaContentBlockParamUnion{ - OfToolResult: &anthropic.BetaToolResultBlockParam{ - ToolUseID: id, - IsError: anthropic.Bool(false), - }, - } - - var hasValidResult bool - for _, content := range res.Content { - switch cb := content.(type) { - case mcp.TextContent: - // messages.Messages = append(messages.Messages, - // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, cb.Text, false)), - //) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: cb.Text, - }, - }) - - hasValidResult = true - case mcp.EmbeddedResource: - // Handle embedded resource based on its type - switch resource := cb.Resource.(type) { - case mcp.TextResourceContents: - // For text resources, include the text content - val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", - resource.MIMEType, resource.URI, resource.Text) - // messages.Messages = append(messages.Messages, - // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, val, false)), - //) - - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: val, - }, - }) - hasValidResult = true - case mcp.BlobResourceContents: - // For blob resources, include the base64 data with MIME type info - val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", - resource.MIMEType, resource.URI, resource.Blob) - // messages.Messages = append(messages.Messages, - // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, val, false)), - //) - - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: val, - }, - }) - hasValidResult = true - default: - b.logger.Error(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource))) - // messages.Messages = append(messages.Messages, - // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, "Error: unknown embedded resource type", true)), - //) - - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: unknown embedded resource type", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - hasValidResult = true - } - default: - // Not supported - but we must still provide a tool_result to match the tool_use - b.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb)), slog.F("json", out.String())) - // messages.Messages = append(messages.Messages, - // anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, "Error: unsupported tool result type", true)), - //) - - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: unsupported tool result type", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - hasValidResult = true - } - } - - // If no content was processed, still add a tool_result - if !hasValidResult { - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: no valid tool result content", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - } - - if len(toolResult.OfToolResult.Content) > 0 { - messages.Messages = append(messages.Messages, anthropic.NewBetaUserMessage(toolResult)) - } - } - - // Causes a new stream to be run with updated messages. - isFirst = false - goto newStream - } else { - // Find all the non-injected tools and track their uses. - for _, block := range message.Content { - switch variant := block.AsAny().(type) { - case anthropic.BetaToolUseBlock: - if b.isInjectedTool(variant.Name) { - continue - } - - b.trackToolUsage(streamCtx, sessionID, message.ID, string(message.Model), variant.Name, variant.Input, false) - } - } - } - } - - // Overwrite response identifier since proxy obscures injected tool call invocations. - event.Message.ID = sessionID - if err := es.TrySend(streamCtx, event); err != nil { - b.logConnectionError(ctx, err, "sending event") - if isConnectionError(err) { - return // Stop processing if client disconnected - } - } + // Process the request. + if err := sess.ProcessRequest(w, r); err != nil { + logger.Error(r.Context(), "session execution failed", slog.Error(err)) } + } +} - var streamErr error - if streamErr = stream.Err(); streamErr != nil { - if isConnectionError(streamErr) { - b.logger.Warn(ctx, "upstream connection closed", slog.Error(streamErr)) - } - - b.logger.Error(ctx, "anthropic stream error", slog.Error(streamErr)) - if antErr := getAnthropicErrorResponse(streamErr); antErr != nil { - err = es.TrySend(streamCtx, antErr) - if err != nil { - b.logger.Error(ctx, "failed to send error", slog.Error(err)) - } +func handleAnthropicMessages(provider *AnthropicMessagesProvider, drpcClient proto.DRPCAIBridgeDaemonClient, tools map[string][]*MCPTool, logger slog.Logger) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Read and parse request. + body, err := io.ReadAll(r.Body) + if err != nil { + if isConnectionError(err) { + logger.Debug(r.Context(), "client disconnected during request body read", slog.Error(err)) + return // Don't send error response if client already disconnected } + logger.Error(r.Context(), "failed to read body", slog.Error(err)) + http.Error(w, "failed to read body", http.StatusInternalServerError) + return } - - err = es.Close(streamCtx) + req, err := provider.ParseRequest(body) if err != nil { - b.logger.Error(ctx, "failed to close event stream", slog.Error(err)) + logger.Error(r.Context(), "failed to parse request", slog.Error(err)) + http.Error(w, "failed to parse request", http.StatusBadRequest) + return } - wg.Wait() - - // Ensure we flush all the remaining data before ending. - flush(w) - - if err != nil || streamErr != nil { - streamCancel(xerrors.Errorf("stream err: %w", err)) + // Create a new session. + var sess Session + if req.UseStreaming() { + sess = provider.NewStreamingSession(req) } else { - streamCancel(xerrors.New("gracefully done")) + sess = provider.NewBlockingSession(req) } - <-streamCtx.Done() - break - } -} - -func (b *Bridge) trackToolUsage(ctx context.Context, sessionID, msgID, model, toolName string, toolInput interface{}, injected bool) { - coderdClient, err := b.clientFn() - if err != nil { - b.logger.Error(ctx, "could not acquire coderd client for tool usage tracking", slog.Error(err)) - return - } + sessID := sess.Init(logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) + logger.Debug(context.Background(), "starting anthropic messages session", slog.F("session_id", sessID)) - var input string - // TODO: unmarshal instead of marshal? i.e. persist maps rather than strings? - switch val := toolInput.(type) { - case string: - input = val - case []byte: - input = string(val) - default: - encoded, err := json.Marshal(toolInput) - if err == nil { - input = string(encoded) - } else { - b.logger.Error(ctx, "failed to marshal tool input", slog.Error(err), slog.F("injected", injected), slog.F("tool_name", toolName)) - input = fmt.Sprintf("%v", val) - } - } + defer func() { + if err := sess.Close(); err != nil { + logger.Warn(context.Background(), "failed to close session", slog.Error(err), slog.F("session_id", sessID), slog.F("kind", fmt.Sprintf("%T", sess))) + } + }() - // For injected tools: strip MCP tool namespacing, if possible. - if injected { - _, tool, err := DecodeToolID(toolName) - // No recourse here; no point in logging - we'll see it in the database anyway. - if err == nil { - toolName = tool + // Process the request. + if err := sess.ProcessRequest(w, r); err != nil { + logger.Error(r.Context(), "session execution failed", slog.Error(err)) } } - - _, err = coderdClient.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: msgID, - Model: model, - Input: input, - Tool: toolName, - Injected: injected, - }) - if err != nil { - b.logger.Error(ctx, "failed to track tool usage", slog.Error(err), slog.F("injected", injected)) - } -} - -func (b *Bridge) trackUserPrompt(ctx context.Context, sessionID, msgID, model, prompt string) { - coderdClient, err := b.clientFn() - if err != nil { - b.logger.Error(ctx, "could not acquire coderd client for user prompt tracking", slog.Error(err)) - return - } - - _, err = coderdClient.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ - SessionId: sessionID, - MsgId: msgID, - Model: model, - Prompt: prompt, - }) - if err != nil { - b.logger.Error(ctx, "failed to track user prompt", slog.Error(err)) - } -} - -func (b *Bridge) trackTokenUsage(ctx context.Context, sessionID, msgID, model string, inputTokens, outputTokens int64, other map[string]int64) { - coderdClient, err := b.clientFn() - if err != nil { - b.logger.Error(ctx, "could not acquire coderd client for token usage tracking", slog.Error(err)) - return - } - - _, err = coderdClient.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ - SessionId: sessionID, - MsgId: msgID, - Model: model, - InputTokens: inputTokens, - OutputTokens: outputTokens, - Other: other, - }) - if err != nil { - b.logger.Error(ctx, "failed to track token usage", slog.Error(err)) - } -} - -func (b *Bridge) isInjectedTool(id string) bool { - _, ok := b.tools[id] - return ok -} - -func getAnthropicErrorResponse(err error) *AnthropicErrorResponse { - var apierr *anthropic.Error - if !errors.As(err, &apierr) { - return nil - } - - msg := apierr.Error() - - var detail *anthropic.BetaAPIError - if field, ok := apierr.JSON.ExtraFields["error"]; ok { - _ = json.Unmarshal([]byte(field.Raw()), &detail) - } - if detail != nil { - msg = detail.Message - } - - return &AnthropicErrorResponse{ - BetaErrorResponse: &anthropic.BetaErrorResponse{ - Error: anthropic.BetaErrorUnion{ - Message: msg, - Type: string(detail.Type), - }, - Type: ant_constant.ValueOf[ant_constant.Error](), - }, - StatusCode: apierr.StatusCode, - } -} - -type AnthropicErrorResponse struct { - *anthropic.BetaErrorResponse - - StatusCode int `json:"-"` -} - -// logConnectionError logs connection errors with appropriate severity -func (b *Bridge) logConnectionError(ctx context.Context, err error, operation string) { - if isConnectionError(err) { - b.logger.Debug(ctx, "client disconnected during "+operation, slog.Error(err)) - } else { - b.logger.Error(ctx, "error during "+operation, slog.Error(err)) - } } diff --git a/aibridged/provider_anthropic_messages.go b/aibridged/provider_anthropic_messages.go new file mode 100644 index 0000000000000..3031fe269fdbf --- /dev/null +++ b/aibridged/provider_anthropic_messages.go @@ -0,0 +1,41 @@ +package aibridged + +import ( + "encoding/json" + + "golang.org/x/xerrors" +) + +var _ Provider[BetaMessageNewParamsWrapper] = &AnthropicMessagesProvider{} + +// AnthropicMessagesProvider allows for interactions with the Anthropic Messages API. +// See https://docs.anthropic.com/en/api/messages +type AnthropicMessagesProvider struct { + baseURL, key string +} + +func NewAnthropicMessagesProvider(baseURL, key string) *AnthropicMessagesProvider { + return &AnthropicMessagesProvider{ + baseURL: baseURL, + key: key, + } +} + +func (*AnthropicMessagesProvider) ParseRequest(payload []byte) (*BetaMessageNewParamsWrapper, error) { + var in BetaMessageNewParamsWrapper + if err := json.Unmarshal(payload, &in); err != nil { + return nil, xerrors.Errorf("failed to unmarshal request: %w", err) + } + + return &in, nil +} + +// NewStreamingSession creates a new session which handles streaming message completions. +func (*AnthropicMessagesProvider) NewStreamingSession(req *BetaMessageNewParamsWrapper) Session { + return NewAnthropicMessagesStreamingSession(req) +} + +// NewBlockingSession creates a new session which handles non-streaming message completions. +func (*AnthropicMessagesProvider) NewBlockingSession(req *BetaMessageNewParamsWrapper) Session { + return NewAnthropicMessagesBlockingSession(req) +} diff --git a/aibridged/session_anthropic_messages_base.go b/aibridged/session_anthropic_messages_base.go new file mode 100644 index 0000000000000..af12c10f3acb3 --- /dev/null +++ b/aibridged/session_anthropic_messages_base.go @@ -0,0 +1,95 @@ +package aibridged + +import ( + "strings" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/google/uuid" + "golang.org/x/xerrors" + + "cdr.dev/slog" +) + +type AnthropicMessagesSessionBase struct { + id string + + req *BetaMessageNewParamsWrapper + + baseURL, key string + logger slog.Logger + + tracker Tracker + toolMgr ToolManager +} + +func (s *AnthropicMessagesSessionBase) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { + s.id = uuid.NewString() + + s.logger = logger.With(slog.F("session_id", s.id)) + + s.baseURL = baseURL + s.key = key + + s.tracker = tracker + s.toolMgr = toolMgr + + return s.id +} + +func (s *AnthropicMessagesSessionBase) LastUserPrompt() (*string, error) { + if s.req == nil { + return nil, xerrors.New("nil request") + } + + return s.req.LastUserPrompt() +} + +func (s *AnthropicMessagesSessionBase) Model() Model { + var model string + if s.req == nil { + model = "?" + } else { + model = string(s.req.Model) + } + + return Model{ + Provider: "anthropic", + ModelName: model, + } +} + +func (s *AnthropicMessagesSessionBase) injectTools() { + if s.req == nil { + return + } + + // Inject tools. + for _, tool := range s.toolMgr.ListTools() { + s.req.Tools = append(s.req.Tools, anthropic.BetaToolUnionParam{ + OfTool: &anthropic.BetaToolParam{ + InputSchema: anthropic.BetaToolInputSchemaParam{ + Properties: tool.Params, + Required: tool.Required, + }, + Name: tool.ID, + Description: anthropic.String(tool.Description), + Type: anthropic.BetaToolTypeCustom, + }, + }) + } + + // Note: Parallel tool calls are disabled to avoid tool_use/tool_result block mismatches. + s.req.ToolChoice = anthropic.BetaToolChoiceUnionParam{ + OfAny: &anthropic.BetaToolChoiceAnyParam{ + Type: "auto", + DisableParallelToolUse: anthropic.Bool(true), + }, + } +} + +// isSmallFastModel checks if the model is a small/fast model (Haiku 3.5). +// These models are optimized for tasks like code autocomplete and other small, quick operations. +// See `ANTHROPIC_SMALL_FAST_MODEL`: https://docs.anthropic.com/en/docs/claude-code/settings#environment-variables +func (s *AnthropicMessagesSessionBase) isSmallFastModel() bool { + return strings.Contains(string(s.req.Model), "3-5-haiku") +} diff --git a/aibridged/session_anthropic_messages_blocking.go b/aibridged/session_anthropic_messages_blocking.go new file mode 100644 index 0000000000000..e89ba3285a8d4 --- /dev/null +++ b/aibridged/session_anthropic_messages_blocking.go @@ -0,0 +1,278 @@ +package aibridged + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/mark3labs/mcp-go/mcp" + "golang.org/x/xerrors" + + "cdr.dev/slog" +) + +var _ Session = &AnthropicMessagesBlockingSession{} + +type AnthropicMessagesBlockingSession struct { + AnthropicMessagesSessionBase +} + +func NewAnthropicMessagesBlockingSession(req *BetaMessageNewParamsWrapper) *AnthropicMessagesBlockingSession { + return &AnthropicMessagesBlockingSession{AnthropicMessagesSessionBase: AnthropicMessagesSessionBase{ + req: req, + }} +} + +func (s *AnthropicMessagesBlockingSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { + return s.AnthropicMessagesSessionBase.Init(logger.Named("blocking"), baseURL, key, tracker, toolMgr) +} + +func (s *AnthropicMessagesBlockingSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { + if s.req == nil { + return xerrors.Errorf("developer error: req is nil") + } + + ctx := r.Context() + + s.injectTools() + + var ( + prompt *string + err error + ) + // Track user prompt if not a small/fast model + if !s.isSmallFastModel() { + prompt, err = s.LastUserPrompt() + if err != nil { + s.logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) + } + } + + // Add beta header if present in the request. + var opts []option.RequestOption + if reqBetaHeader := r.Header.Get("anthropic-beta"); strings.TrimSpace(reqBetaHeader) != "" { + opts = append(opts, option.WithHeader("anthropic-beta", reqBetaHeader)) + } + opts = append(opts, option.WithRequestTimeout(time.Second*30)) + + client := newAnthropicClient(s.baseURL, s.key, opts...) // TODO: configurable timeout + messages := s.req.BetaMessageNewParams + logger := s.logger.With(slog.F("model", s.req.Model)) + + var resp *anthropic.BetaMessage + + for { + resp, err = client.Beta.Messages.New(ctx, messages) + if err != nil { + if isConnectionError(err) { + logger.Warn(ctx, "upstream connection closed", slog.Error(err)) + return xerrors.Errorf("upstream connection closed: %w", err) + } + + logger.Error(ctx, "anthropic API error", slog.Error(err)) + if antErr := getAnthropicErrorResponse(err); antErr != nil { + http.Error(w, antErr.Error.Message, antErr.StatusCode) + return xerrors.Errorf("api error: %w", err) + } + + logger.Error(ctx, "upstream API error", slog.Error(err)) + http.Error(w, "internal error", http.StatusInternalServerError) + return xerrors.Errorf("upstream API error: %w", err) + } + + if prompt != nil { + if err := s.tracker.TrackPromptUsage(ctx, s.id, resp.ID, s.Model(), *prompt, nil); err != nil { + s.logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) + } + } + + if err := s.tracker.TrackTokensUsage(ctx, s.id, resp.ID, s.Model(), resp.Usage.InputTokens, resp.Usage.OutputTokens, Metadata{ + "web_search_requests": resp.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": resp.Usage.CacheCreationInputTokens, + "cache_read_input": resp.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": resp.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": resp.Usage.CacheCreation.Ephemeral5mInputTokens, + }); err != nil { + logger.Warn(ctx, "failed to track token usage", slog.Error(err)) + } + + // Handle tool calls for non-streaming. + var pendingToolCalls []anthropic.BetaToolUseBlock + for _, c := range resp.Content { + toolUse := c.AsToolUse() + if toolUse.ID == "" { + continue + } + + if s.toolMgr.GetTool(toolUse.Name) != nil { + pendingToolCalls = append(pendingToolCalls, toolUse) + continue + } + + // If tool is not injected, track it since the client will be handling it. + if err := s.tracker.TrackToolUsage(ctx, s.id, resp.ID, s.Model(), toolUse.Name, toolUse.Input, false, nil); err != nil { + logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) + } + } + + // If no injected tool calls, we're done. + if len(pendingToolCalls) == 0 { + break + } + + // Append the assistant's message (which contains the tool_use block) + // to the messages for the next API call. + messages.Messages = append(messages.Messages, resp.ToParam()) + + // Process each pending tool call. + for _, tc := range pendingToolCalls { + tool := s.toolMgr.GetTool(tc.Name) + if tool == nil { + logger.Warn(ctx, "tool not found in manager", slog.F("tool", tc.Name)) + // Continue to next tool call, but still append an error tool_result + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error: tool %s not found", tc.Name), true)), + ) + continue + } + + var args map[string]any + serialized, err := json.Marshal(tc.Input) + if err != nil { + logger.Warn(ctx, "failed to marshal tool args for unmarshal", slog.Error(err), slog.F("tool", tc.Name)) + // Continue to next tool call, but still append an error tool_result + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error unmarshaling tool arguments: %v", err), true)), + ) + continue + } else if err := json.Unmarshal(serialized, &args); err != nil { + logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", tc.Name)) + // Continue to next tool call, but still append an error tool_result + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error unmarshaling tool arguments: %v", err), true)), + ) + continue + } + + // Track injected tool usage - strip MCP tool namespacing if possible + toolName := tc.Name + if _, tool, err := DecodeToolID(toolName); err == nil { + toolName = tool + } + if err := s.tracker.TrackToolUsage(ctx, s.id, resp.ID, s.Model(), toolName, args, true, nil); err != nil { + logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) + } + + res, err := tool.Call(ctx, args) + if err != nil { + // Always provide a tool_result even if the tool call failed + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error calling tool: %v", err), true)), + ) + continue + } + + // Process tool result + toolResult := anthropic.BetaContentBlockParamUnion{ + OfToolResult: &anthropic.BetaToolResultBlockParam{ + ToolUseID: tc.ID, + IsError: anthropic.Bool(false), + }, + } + + var hasValidResult bool + for _, content := range res.Content { + switch cb := content.(type) { + case mcp.TextContent: + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: cb.Text, + }, + }) + hasValidResult = true + case mcp.EmbeddedResource: + switch resource := cb.Resource.(type) { + case mcp.TextResourceContents: + val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", + resource.MIMEType, resource.URI, resource.Text) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: val, + }, + }) + hasValidResult = true + case mcp.BlobResourceContents: + val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", + resource.MIMEType, resource.URI, resource.Blob) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: val, + }, + }) + hasValidResult = true + default: + s.logger.Error(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource))) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: unknown embedded resource type", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + hasValidResult = true + } + default: + s.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb))) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: unsupported tool result type", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + hasValidResult = true + } + } + + // If no content was processed, still add a tool_result + if !hasValidResult { + s.logger.Error(ctx, "no tool result added", slog.F("content_len", len(res.Content)), slog.F("is_error", res.IsError)) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: no valid tool result content", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + } + + if len(toolResult.OfToolResult.Content) > 0 { + messages.Messages = append(messages.Messages, anthropic.NewBetaUserMessage(toolResult)) + } + } + } + + if resp == nil { + return nil + } + + // Overwrite response identifier since proxy obscures injected tool call invocations. + resp.ID = s.id + + out, err := json.Marshal(resp) + if err != nil { + http.Error(w, "error marshaling response", http.StatusInternalServerError) + return xerrors.Errorf("failed to marshal response: %w", err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(out) + + return nil +} + +func (s *AnthropicMessagesBlockingSession) Close() error { + return nil +} diff --git a/aibridged/session_anthropic_messages_streaming.go b/aibridged/session_anthropic_messages_streaming.go new file mode 100644 index 0000000000000..ade8c9bbcd696 --- /dev/null +++ b/aibridged/session_anthropic_messages_streaming.go @@ -0,0 +1,386 @@ +package aibridged + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" + "github.com/mark3labs/mcp-go/mcp" + "github.com/openai/openai-go/shared/constant" + "golang.org/x/xerrors" + + "cdr.dev/slog" +) + +var _ Session = &AnthropicMessagesStreamingSession{} + +type AnthropicMessagesStreamingSession struct { + AnthropicMessagesSessionBase +} + +func NewAnthropicMessagesStreamingSession(req *BetaMessageNewParamsWrapper) *AnthropicMessagesStreamingSession { + return &AnthropicMessagesStreamingSession{AnthropicMessagesSessionBase: AnthropicMessagesSessionBase{ + req: req, + }} +} + +func (s *AnthropicMessagesStreamingSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { + return s.AnthropicMessagesSessionBase.Init(logger.Named("streaming"), baseURL, key, tracker, toolMgr) +} + +func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { + if s.req == nil { + return xerrors.Errorf("developer error: req is nil") + } + + // Allow us to interrupt watch via cancel. + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + r = r.WithContext(ctx) // Rewire context for SSE cancellation. + + s.injectTools() + + // Track user prompt if not a small/fast model + if !s.isSmallFastModel() { + prompt, err := s.LastUserPrompt() + if err != nil { + s.logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) + } else if prompt != nil { + if err := s.tracker.TrackPromptUsage(ctx, s.id, "", s.Model(), *prompt, nil); err != nil { + s.logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) + } + } + } + + streamCtx, streamCancel := context.WithCancelCause(ctx) + defer streamCancel(xerrors.New("deferred")) + + es := newEventStream(anthropicEventStream) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + defer func() { + if err := es.Close(streamCtx); err != nil { + s.logger.Error(ctx, "error closing stream", slog.Error(err)) + } + }() + + BasicSSESender(streamCtx, es, s.logger.Named("sse-sender")).ServeHTTP(w, r) + }() + + // Add beta header if present in the request. + var opts []option.RequestOption + if reqBetaHeader := r.Header.Get("anthropic-beta"); strings.TrimSpace(reqBetaHeader) != "" { + opts = append(opts, option.WithHeader("anthropic-beta", reqBetaHeader)) + } + + client := newAnthropicClient(s.baseURL, s.key, opts...) + messages := s.req.BetaMessageNewParams + logger := s.logger.With(slog.F("model", s.req.Model)) + + isFirst := true + for { + newStream: + stream := client.Beta.Messages.NewStreaming(streamCtx, messages) + + var message anthropic.BetaMessage + var lastToolName string + + pendingToolCalls := make(map[string]string) + + for stream.Next() { + event := stream.Current() + if err := message.Accumulate(event); err != nil { + logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) + http.Error(w, "failed to proxy request", http.StatusInternalServerError) + return xerrors.Errorf("failed to accumulate streaming events: %w", err) + } + + // Tool-related handling. + switch event.Type { + case string(constant.ValueOf[ant_constant.ContentBlockStart]()): + switch block := event.AsContentBlockStart().ContentBlock.AsAny().(type) { + case anthropic.BetaToolUseBlock: + lastToolName = block.Name + + if s.toolMgr.GetTool(block.Name) != nil { + pendingToolCalls[block.Name] = block.ID + // Don't relay this event back, otherwise the client will try invoke the tool as well. + continue + } + } + case string(constant.ValueOf[ant_constant.ContentBlockDelta]()): + if len(pendingToolCalls) > 0 && s.toolMgr.GetTool(lastToolName) != nil { + // We're busy with a tool call, don't relay this event back. + continue + } + case string(constant.ValueOf[ant_constant.ContentBlockStop]()): + // Reset the tool name + isInjected := s.toolMgr.GetTool(lastToolName) != nil + lastToolName = "" + + if len(pendingToolCalls) > 0 && isInjected { + // We're busy with a tool call, don't relay this event back. + continue + } + case string(ant_constant.ValueOf[ant_constant.MessageStart]()): + // Track token usage + start := event.AsMessageStart() + metadata := Metadata{ + "web_search_requests": start.Message.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": start.Message.Usage.CacheCreationInputTokens, + "cache_read_input": start.Message.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": start.Message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": start.Message.Usage.CacheCreation.Ephemeral5mInputTokens, + } + if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, s.Model(), start.Message.Usage.InputTokens, start.Message.Usage.OutputTokens, metadata); err != nil { + logger.Warn(ctx, "failed to track token usage", slog.Error(err)) + } + + if !isFirst { + // Don't send message_start unless first message! + // We're sending multiple messages back and forth with the API, but from the client's perspective + // they're just expecting a single message. + continue + } + case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): + delta := event.AsMessageDelta() + // Track token usage + metadata := Metadata{ + "web_search_requests": delta.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": delta.Usage.CacheCreationInputTokens, + "cache_read_input": delta.Usage.CacheReadInputTokens, + // Note: CacheCreation fields are not available in MessageDeltaUsage + "cache_ephemeral_1h_input": 0, + "cache_ephemeral_5m_input": 0, + } + if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, s.Model(), delta.Usage.InputTokens, delta.Usage.OutputTokens, metadata); err != nil { + logger.Warn(ctx, "failed to track token usage", slog.Error(err)) + } + + // Don't relay message_delta events which indicate injected tool use. + if len(pendingToolCalls) > 0 && s.toolMgr.GetTool(lastToolName) != nil { + continue + } + + // If currently calling a tool. + if len(message.Content) > 0 && message.Content[len(message.Content)-1].Type == string(ant_constant.ValueOf[ant_constant.ToolUse]()) { + toolName := message.Content[len(message.Content)-1].AsToolUse().Name + if len(pendingToolCalls) > 0 && s.toolMgr.GetTool(toolName) != nil { + continue + } + } + + // Don't send message_stop until all tools have been called. + case string(ant_constant.ValueOf[ant_constant.MessageStop]()): + + if len(pendingToolCalls) > 0 { + // Append the whole message from this stream as context since we'll be sending a new request with the tool results. + messages.Messages = append(messages.Messages, message.ToParam()) + + for name, id := range pendingToolCalls { + if s.toolMgr.GetTool(name) == nil { + // Not an MCP proxy call, don't do anything. + continue + } + + tool := s.toolMgr.GetTool(name) + if tool == nil { + logger.Error(ctx, "tool not found in manager", slog.F("tool_name", name)) + continue + } + + var ( + input any + foundTool bool + foundTools int + ) + for _, block := range message.Content { + switch variant := block.AsAny().(type) { + case anthropic.BetaToolUseBlock: + foundTools++ + if variant.Name == name { + input = variant.Input + foundTool = true + } + } + } + + if !foundTool { + logger.Error(ctx, "failed to find tool input", slog.F("tool_name", name), slog.F("found_tools", foundTools)) + continue + } + + // Track injected tool usage - strip MCP tool namespacing if possible + toolName := tool.Name + if _, decodedTool, err := DecodeToolID(toolName); err == nil { + toolName = decodedTool + } + if err := s.tracker.TrackToolUsage(streamCtx, s.id, message.ID, s.Model(), toolName, input, true, nil); err != nil { + logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) + } + + res, err := tool.Call(streamCtx, input) + if err != nil { + // Always provide a tool_result even if the tool call failed + messages.Messages = append(messages.Messages, + anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, fmt.Sprintf("Error calling tool: %v", err), true)), + ) + continue + } + + // Process tool result + toolResult := anthropic.BetaContentBlockParamUnion{ + OfToolResult: &anthropic.BetaToolResultBlockParam{ + ToolUseID: id, + IsError: anthropic.Bool(false), + }, + } + + var hasValidResult bool + for _, content := range res.Content { + switch cb := content.(type) { + case mcp.TextContent: + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: cb.Text, + }, + }) + hasValidResult = true + case mcp.EmbeddedResource: + switch resource := cb.Resource.(type) { + case mcp.TextResourceContents: + val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", + resource.MIMEType, resource.URI, resource.Text) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: val, + }, + }) + hasValidResult = true + case mcp.BlobResourceContents: + val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", + resource.MIMEType, resource.URI, resource.Blob) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: val, + }, + }) + hasValidResult = true + default: + s.logger.Error(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource))) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: unknown embedded resource type", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + hasValidResult = true + } + default: + s.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb))) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: unsupported tool result type", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + hasValidResult = true + } + } + + // If no content was processed, still add a tool_result + if !hasValidResult { + s.logger.Error(ctx, "no tool result added", slog.F("content_len", len(res.Content)), slog.F("is_error", res.IsError)) + toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: "Error: no valid tool result content", + }, + }) + toolResult.OfToolResult.IsError = anthropic.Bool(true) + } + + if len(toolResult.OfToolResult.Content) > 0 { + messages.Messages = append(messages.Messages, anthropic.NewBetaUserMessage(toolResult)) + } + } + + // Causes a new stream to be run with updated messages. + isFirst = false + goto newStream + } else { + // Find all the non-injected tools and track their uses. + for _, block := range message.Content { + switch variant := block.AsAny().(type) { + case anthropic.BetaToolUseBlock: + if s.toolMgr.GetTool(variant.Name) != nil { + continue + } + + if err := s.tracker.TrackToolUsage(streamCtx, s.id, message.ID, s.Model(), variant.Name, variant.Input, false, nil); err != nil { + logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) + } + } + } + } + } + + // Overwrite response identifier since proxy obscures injected tool call invocations. + event.Message.ID = s.id + if err := es.TrySend(streamCtx, event); err != nil { + if isConnectionError(err) { + s.logger.Debug(ctx, "client disconnected during sending event", slog.Error(err)) + return nil // Stop processing if client disconnected + } else { + s.logger.Error(ctx, "error during sending event", slog.Error(err)) + } + } + } + + var streamErr error + if streamErr = stream.Err(); streamErr != nil { + if isConnectionError(streamErr) { + logger.Warn(ctx, "upstream connection closed", slog.Error(streamErr)) + } else { + logger.Error(ctx, "anthropic stream error", slog.Error(streamErr)) + if antErr := getAnthropicErrorResponse(streamErr); antErr != nil { + if err := es.TrySend(streamCtx, antErr); err != nil { + logger.Error(ctx, "failed to send error", slog.Error(err)) + } + } + } + } + + if err := es.Close(streamCtx); err != nil { + logger.Error(ctx, "failed to close event stream", slog.Error(err)) + } + + wg.Wait() + + // Ensure we flush all the remaining data before ending. + flush(w) + + if streamErr != nil { + streamCancel(xerrors.Errorf("stream err: %w", streamErr)) + } else { + streamCancel(xerrors.New("gracefully done")) + } + + <-streamCtx.Done() + break + } + + return nil +} + +func (s *AnthropicMessagesStreamingSession) Close() error { + return nil +} From 069d66db472a0c36124e70fa802f67230c186ea5 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 8 Aug 2025 16:06:09 +0200 Subject: [PATCH 54/61] cheers wormhole! Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 10 + aibridged/bridge.go | 46 +- aibridged/bridge_integration_test.go | 41 +- aibridged/middleware.go | 9 +- aibridged/proto/aibridged.pb.go | 460 ++++++++++++------ aibridged/proto/aibridged.proto | 24 +- aibridged/proto/aibridged_drpc.pb.go | 46 +- aibridged/session.go | 2 +- aibridged/session_anthropic_messages_base.go | 7 +- .../session_anthropic_messages_blocking.go | 4 +- .../session_anthropic_messages_streaming.go | 4 +- aibridged/session_openai_chat_base.go | 7 +- aibridged/session_openai_chat_blocking.go | 4 +- aibridged/session_openai_chat_streaming.go | 4 +- aibridged/tracker.go | 25 +- coderd/aibridgedserver/aibridgedserver.go | 105 +++- coderd/apidoc/docs.go | 28 +- coderd/apidoc/swagger.json | 28 +- coderd/coderd.go | 2 +- coderd/database/dbauthz/dbauthz.go | 24 +- coderd/database/dbmetrics/querymetrics.go | 35 +- coderd/database/dbmock/dbmock.go | 71 ++- coderd/database/dump.sql | 87 +++- coderd/database/foreign_key_constraint.go | 4 + .../migrations/000357_aibridge.down.sql | 4 + .../migrations/000357_aibridge.up.sql | 55 ++- coderd/database/models.go | 45 +- coderd/database/querier.go | 5 +- coderd/database/queries.sql.go | 127 ++++- coderd/database/queries/aibridge.sql | 25 + coderd/database/queries/wormhole.sql | 3 - coderd/database/unique_constraint.go | 4 + docs/reference/api/general.md | 10 +- docs/reference/api/schemas.md | 82 +++- 34 files changed, 1145 insertions(+), 292 deletions(-) create mode 100644 coderd/database/migrations/000357_aibridge.down.sql create mode 100644 coderd/database/queries/aibridge.sql delete mode 100644 coderd/database/queries/wormhole.sql diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 9d9a1d77dca39..98ae8e3f031e2 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -149,6 +149,16 @@ func (s *Server) Client() (proto.DRPCAIBridgeDaemonClient, error) { } } +func (s *Server) StartSession(ctx context.Context, in *proto.StartSessionRequest) (*proto.StartSessionResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.StartSessionResponse, error) { + return client.StartSession(ctx, in) + }) + if err != nil { + return nil, err + } + return out, nil +} + func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackTokenUsageResponse, error) { return client.TrackTokenUsage(ctx, in) diff --git a/aibridged/bridge.go b/aibridged/bridge.go index bf610c4b4a06a..73707361bba85 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -11,6 +11,8 @@ import ( "cdr.dev/slog" + "github.com/google/uuid" + "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/codersdk" ) @@ -109,7 +111,27 @@ func handleOpenAIChat(provider *OpenAIChatProvider, drpcClient proto.DRPCAIBridg sess = provider.NewBlockingSession(req) } - sessID := sess.Init(logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) + userID, ok := r.Context().Value(ContextKeyBridgeUserID{}).(uuid.UUID) + if !ok { + logger.Error(r.Context(), "missing initiator ID in context") + http.Error(w, "unable to retrieve initiator", http.StatusInternalServerError) + return + } + + resp, err := drpcClient.StartSession(r.Context(), &proto.StartSessionRequest{ + InitiatorId: userID.String(), + Provider: "openai", + Model: req.Model, + }) + if err != nil { + logger.Error(r.Context(), "failed to start session", slog.Error(err)) + http.Error(w, "failed to start session", http.StatusInternalServerError) + return + } + + sessID := resp.GetSessionId() + + sess.Init(sessID, logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) logger.Debug(context.Background(), "starting openai session", slog.F("session_id", sessID)) defer func() { @@ -153,7 +175,27 @@ func handleAnthropicMessages(provider *AnthropicMessagesProvider, drpcClient pro sess = provider.NewBlockingSession(req) } - sessID := sess.Init(logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) + userID, ok := r.Context().Value(ContextKeyBridgeUserID{}).(uuid.UUID) + if !ok { + logger.Error(r.Context(), "missing initiator ID in context") + http.Error(w, "unable to retrieve initiator", http.StatusInternalServerError) + return + } + + resp, err := drpcClient.StartSession(r.Context(), &proto.StartSessionRequest{ + InitiatorId: userID.String(), + Provider: "anthropic", + Model: string(req.Model), + }) + if err != nil { + logger.Error(r.Context(), "failed to start session", slog.Error(err)) + http.Error(w, "failed to start session", http.StatusInternalServerError) + return + } + + sessID := resp.GetSessionId() + + sess.Init(sessID, logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) logger.Debug(context.Background(), "starting anthropic messages session", slog.F("session_id", sessID)) defer func() { diff --git a/aibridged/bridge_integration_test.go b/aibridged/bridge_integration_test.go index 17207ede87a7e..3fbeceea152c6 100644 --- a/aibridged/bridge_integration_test.go +++ b/aibridged/bridge_integration_test.go @@ -130,7 +130,7 @@ func TestAnthropicMessages(t *testing.T) { }, nil) require.NoError(t, err) - mockSrv := httptest.NewServer(b.Handler()) + mockSrv := httptest.NewServer(withInitiator(getCurrentUserID(t, client), b.Handler())) // Make API call to aibridge for Anthropic /v1/messages req := createAnthropicMessagesReq(t, mockSrv.URL, reqBody) client := &http.Client{} @@ -168,7 +168,6 @@ func TestAnthropicMessages(t *testing.T) { }) } }) - } func TestOpenAIChatCompletions(t *testing.T) { @@ -234,7 +233,7 @@ func TestOpenAIChatCompletions(t *testing.T) { }, nil) require.NoError(t, err) - mockSrv := httptest.NewServer(b.Handler()) + mockSrv := httptest.NewServer(withInitiator(getCurrentUserID(t, client), b.Handler())) // Make API call to aibridge for OpenAI /v1/chat/completions req := createOpenAIChatCompletionsReq(t, mockSrv.URL, reqBody) @@ -275,7 +274,6 @@ func TestOpenAIChatCompletions(t *testing.T) { }) } }) - } func TestSimple(t *testing.T) { @@ -426,7 +424,7 @@ func TestSimple(t *testing.T) { b, err := tc.configureFunc(srv.URL, coderdClient) require.NoError(t, err) - mockSrv := httptest.NewServer(b.Handler()) + mockSrv := httptest.NewServer(withInitiator(getCurrentUserID(t, client), b.Handler())) // When: calling the "API server" with the fixture's request body. req := tc.createRequest(t, mockSrv.URL, reqBody) client := &http.Client{} @@ -681,7 +679,7 @@ func TestInjectedTool(t *testing.T) { require.NoError(t, err) // Invoke request to mocked API via aibridge. - bridgeSrv := httptest.NewServer(b.Handler()) + bridgeSrv := httptest.NewServer(withInitiator(getCurrentUserID(t, client), b.Handler())) t.Cleanup(bridgeSrv.Close) req := tc.createRequest(t, bridgeSrv.URL, reqBody) @@ -846,9 +844,12 @@ func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap, resp return ms } +var _ proto.DRPCAIBridgeDaemonClient = &fakeBridgeDaemonClient{} + type fakeBridgeDaemonClient struct { mu sync.Mutex + sessions []*proto.StartSessionRequest tokenUsages []*proto.TrackTokenUsageRequest userPrompts []*proto.TrackUserPromptRequest toolUsages []*proto.TrackToolUsageRequest @@ -859,6 +860,17 @@ func (*fakeBridgeDaemonClient) DRPCConn() drpc.Conn { return conn } +// StartSession implements proto.DRPCAIBridgeDaemonClient. +func (f *fakeBridgeDaemonClient) StartSession(ctx context.Context, in *proto.StartSessionRequest) (*proto.StartSessionResponse, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.sessions = append(f.sessions, in) + + return &proto.StartSessionResponse{ + SessionId: uuid.NewString(), + }, nil +} + func (f *fakeBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { f.mu.Lock() defer f.mu.Unlock() @@ -903,3 +915,20 @@ func createMockMCPSrv(t *testing.T) http.Handler { return server.NewStreamableHTTPServer(s) } + +// withInitiator wraps a handler injecting the Bridge user ID into context. +// TODO: this is only necessary because we're not exercising the real API's middleware, which may hide some problems. +func withInitiator(userID uuid.UUID, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), aibridged.ContextKeyBridgeUserID{}, userID) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getCurrentUserID(t *testing.T, client *codersdk.Client) uuid.UUID { + t.Helper() + + me, err := client.User(t.Context(), "me") + require.NoError(t, err) + return me.ID +} diff --git a/aibridged/middleware.go b/aibridged/middleware.go index 23bb9d6a2f1ef..16a25af11a7ce 100644 --- a/aibridged/middleware.go +++ b/aibridged/middleware.go @@ -11,6 +11,7 @@ import ( ) type ContextKeyBridgeAPIKey struct{} +type ContextKeyBridgeUserID struct{} // AuthMiddleware extracts and validates authorization tokens for AI bridge endpoints. // It supports both Bearer tokens in Authorization headers and Coder session tokens @@ -28,7 +29,7 @@ func AuthMiddleware(db database.Store) func(http.Handler) http.Handler { } // Validate token using httpmw.APIKeyFromRequest - _, _, ok := httpmw.APIKeyFromRequest(ctx, db, func(r *http.Request) string { + key, _, ok := httpmw.APIKeyFromRequest(ctx, db, func(r *http.Request) string { return token }, &http.Request{}) @@ -37,8 +38,12 @@ func AuthMiddleware(db database.Store) func(http.Handler) http.Handler { return } + ctx = context.WithValue( + context.WithValue(ctx, ContextKeyBridgeUserID{}, key.UserID), + ContextKeyBridgeAPIKey{}, token) + // Pass request with modify context including the request token. - next.ServeHTTP(rw, r.WithContext(context.WithValue(ctx, ContextKeyBridgeAPIKey{}, token))) + next.ServeHTTP(rw, r.WithContext(ctx)) }) } } diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index 90a5391db5619..4760f31fafbd5 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -9,6 +9,7 @@ package proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" reflect "reflect" sync "sync" ) @@ -20,23 +21,132 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type StartSessionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + InitiatorId string `protobuf:"bytes,1,opt,name=initiator_id,json=initiatorId,proto3" json:"initiator_id,omitempty"` + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + Model string `protobuf:"bytes,3,opt,name=model,proto3" json:"model,omitempty"` +} + +func (x *StartSessionRequest) Reset() { + *x = StartSessionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StartSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartSessionRequest) ProtoMessage() {} + +func (x *StartSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StartSessionRequest.ProtoReflect.Descriptor instead. +func (*StartSessionRequest) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} +} + +func (x *StartSessionRequest) GetInitiatorId() string { + if x != nil { + return x.InitiatorId + } + return "" +} + +func (x *StartSessionRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *StartSessionRequest) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + +type StartSessionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` +} + +func (x *StartSessionResponse) Reset() { + *x = StartSessionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StartSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartSessionResponse) ProtoMessage() {} + +func (x *StartSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StartSessionResponse.ProtoReflect.Descriptor instead. +func (*StartSessionResponse) Descriptor() ([]byte, []int) { + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} +} + +func (x *StartSessionResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + type TrackTokenUsageRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` - InputTokens int64 `protobuf:"varint,3,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` - OutputTokens int64 `protobuf:"varint,4,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` - Model string `protobuf:"bytes,5,opt,name=model,proto3" json:"model,omitempty"` - Other map[string]int64 `protobuf:"bytes,6,rep,name=other,proto3" json:"other,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` + InputTokens int64 `protobuf:"varint,3,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` + OutputTokens int64 `protobuf:"varint,4,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` + Metadata map[string]*anypb.Any `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *TrackTokenUsageRequest) Reset() { *x = TrackTokenUsageRequest{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -49,7 +159,7 @@ func (x *TrackTokenUsageRequest) String() string { func (*TrackTokenUsageRequest) ProtoMessage() {} func (x *TrackTokenUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -62,7 +172,7 @@ func (x *TrackTokenUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackTokenUsageRequest.ProtoReflect.Descriptor instead. func (*TrackTokenUsageRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} } func (x *TrackTokenUsageRequest) GetSessionId() string { @@ -93,16 +203,9 @@ func (x *TrackTokenUsageRequest) GetOutputTokens() int64 { return 0 } -func (x *TrackTokenUsageRequest) GetModel() string { +func (x *TrackTokenUsageRequest) GetMetadata() map[string]*anypb.Any { if x != nil { - return x.Model - } - return "" -} - -func (x *TrackTokenUsageRequest) GetOther() map[string]int64 { - if x != nil { - return x.Other + return x.Metadata } return nil } @@ -116,7 +219,7 @@ type TrackTokenUsageResponse struct { func (x *TrackTokenUsageResponse) Reset() { *x = TrackTokenUsageResponse{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -129,7 +232,7 @@ func (x *TrackTokenUsageResponse) String() string { func (*TrackTokenUsageResponse) ProtoMessage() {} func (x *TrackTokenUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -142,7 +245,7 @@ func (x *TrackTokenUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackTokenUsageResponse.ProtoReflect.Descriptor instead. func (*TrackTokenUsageResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{3} } type TrackUserPromptRequest struct { @@ -150,16 +253,16 @@ type TrackUserPromptRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` - Prompt string `protobuf:"bytes,3,opt,name=prompt,proto3" json:"prompt,omitempty"` - Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` + Prompt string `protobuf:"bytes,3,opt,name=prompt,proto3" json:"prompt,omitempty"` + Metadata map[string]*anypb.Any `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *TrackUserPromptRequest) Reset() { *x = TrackUserPromptRequest{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -172,7 +275,7 @@ func (x *TrackUserPromptRequest) String() string { func (*TrackUserPromptRequest) ProtoMessage() {} func (x *TrackUserPromptRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -185,7 +288,7 @@ func (x *TrackUserPromptRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackUserPromptRequest.ProtoReflect.Descriptor instead. func (*TrackUserPromptRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{4} } func (x *TrackUserPromptRequest) GetSessionId() string { @@ -209,11 +312,11 @@ func (x *TrackUserPromptRequest) GetPrompt() string { return "" } -func (x *TrackUserPromptRequest) GetModel() string { +func (x *TrackUserPromptRequest) GetMetadata() map[string]*anypb.Any { if x != nil { - return x.Model + return x.Metadata } - return "" + return nil } type TrackUserPromptResponse struct { @@ -225,7 +328,7 @@ type TrackUserPromptResponse struct { func (x *TrackUserPromptResponse) Reset() { *x = TrackUserPromptResponse{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -238,7 +341,7 @@ func (x *TrackUserPromptResponse) String() string { func (*TrackUserPromptResponse) ProtoMessage() {} func (x *TrackUserPromptResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -251,7 +354,7 @@ func (x *TrackUserPromptResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackUserPromptResponse.ProtoReflect.Descriptor instead. func (*TrackUserPromptResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{3} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} } type TrackToolUsageRequest struct { @@ -259,18 +362,18 @@ type TrackToolUsageRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` - Tool string `protobuf:"bytes,3,opt,name=tool,proto3" json:"tool,omitempty"` - Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` - Input string `protobuf:"bytes,5,opt,name=input,proto3" json:"input,omitempty"` - Injected bool `protobuf:"varint,6,opt,name=injected,proto3" json:"injected,omitempty"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` + Tool string `protobuf:"bytes,3,opt,name=tool,proto3" json:"tool,omitempty"` + Input string `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` + Injected bool `protobuf:"varint,5,opt,name=injected,proto3" json:"injected,omitempty"` + Metadata map[string]*anypb.Any `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *TrackToolUsageRequest) Reset() { *x = TrackToolUsageRequest{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -283,7 +386,7 @@ func (x *TrackToolUsageRequest) String() string { func (*TrackToolUsageRequest) ProtoMessage() {} func (x *TrackToolUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -296,7 +399,7 @@ func (x *TrackToolUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackToolUsageRequest.ProtoReflect.Descriptor instead. func (*TrackToolUsageRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{4} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{6} } func (x *TrackToolUsageRequest) GetSessionId() string { @@ -320,13 +423,6 @@ func (x *TrackToolUsageRequest) GetTool() string { return "" } -func (x *TrackToolUsageRequest) GetModel() string { - if x != nil { - return x.Model - } - return "" -} - func (x *TrackToolUsageRequest) GetInput() string { if x != nil { return x.Input @@ -341,6 +437,13 @@ func (x *TrackToolUsageRequest) GetInjected() bool { return false } +func (x *TrackToolUsageRequest) GetMetadata() map[string]*anypb.Any { + if x != nil { + return x.Metadata + } + return nil +} + type TrackToolUsageResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -350,7 +453,7 @@ type TrackToolUsageResponse struct { func (x *TrackToolUsageResponse) Reset() { *x = TrackToolUsageResponse{} if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -363,7 +466,7 @@ func (x *TrackToolUsageResponse) String() string { func (*TrackToolUsageResponse) ProtoMessage() {} func (x *TrackToolUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] + mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -376,7 +479,7 @@ func (x *TrackToolUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackToolUsageResponse.ProtoReflect.Descriptor instead. func (*TrackToolUsageResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} + return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{7} } var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor @@ -384,71 +487,106 @@ var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x0a, 0x1f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x09, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x22, 0xaa, 0x02, 0x0a, - 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, 0x0a, - 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, - 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x42, 0x0a, 0x05, 0x6f, - 0x74, 0x68, 0x65, 0x72, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x69, 0x62, - 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, 0x74, - 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x1a, - 0x38, 0x0a, 0x0a, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, - 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x7c, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, - 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, - 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, - 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, - 0x73, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, - 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, - 0x65, 0x6c, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa9, 0x01, - 0x0a, 0x15, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x12, 0x0a, - 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, - 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, - 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, - 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x32, 0x9b, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, - 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, - 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, - 0x6d, 0x70, 0x74, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, - 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, - 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, - 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, - 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, - 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, - 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, + 0x6f, 0x12, 0x09, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x1a, 0x19, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6a, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x72, 0x74, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, + 0x0a, 0x0c, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x49, + 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, + 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, + 0x64, 0x65, 0x6c, 0x22, 0x35, 0x0a, 0x14, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xb6, 0x02, 0x0a, 0x16, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x23, + 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x12, 0x4b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, + 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x86, + 0x02, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, + 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x4b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x69, 0x62, 0x72, + 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, + 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, + 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0xb2, 0x02, 0x0a, 0x15, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, + 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, + 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, + 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x4a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x69, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x32, 0xec, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4f, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, + 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, + 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, + 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, + 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, + 0x70, 0x74, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, + 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, 0x61, + 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x69, + 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, + 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, + 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, + 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, + 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -463,29 +601,41 @@ func file_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { return file_aibridged_proto_aibridged_proto_rawDescData } -var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ - (*TrackTokenUsageRequest)(nil), // 0: aibridged.TrackTokenUsageRequest - (*TrackTokenUsageResponse)(nil), // 1: aibridged.TrackTokenUsageResponse - (*TrackUserPromptRequest)(nil), // 2: aibridged.TrackUserPromptRequest - (*TrackUserPromptResponse)(nil), // 3: aibridged.TrackUserPromptResponse - (*TrackToolUsageRequest)(nil), // 4: aibridged.TrackToolUsageRequest - (*TrackToolUsageResponse)(nil), // 5: aibridged.TrackToolUsageResponse - nil, // 6: aibridged.TrackTokenUsageRequest.OtherEntry + (*StartSessionRequest)(nil), // 0: aibridged.StartSessionRequest + (*StartSessionResponse)(nil), // 1: aibridged.StartSessionResponse + (*TrackTokenUsageRequest)(nil), // 2: aibridged.TrackTokenUsageRequest + (*TrackTokenUsageResponse)(nil), // 3: aibridged.TrackTokenUsageResponse + (*TrackUserPromptRequest)(nil), // 4: aibridged.TrackUserPromptRequest + (*TrackUserPromptResponse)(nil), // 5: aibridged.TrackUserPromptResponse + (*TrackToolUsageRequest)(nil), // 6: aibridged.TrackToolUsageRequest + (*TrackToolUsageResponse)(nil), // 7: aibridged.TrackToolUsageResponse + nil, // 8: aibridged.TrackTokenUsageRequest.MetadataEntry + nil, // 9: aibridged.TrackUserPromptRequest.MetadataEntry + nil, // 10: aibridged.TrackToolUsageRequest.MetadataEntry + (*anypb.Any)(nil), // 11: google.protobuf.Any } var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ - 6, // 0: aibridged.TrackTokenUsageRequest.other:type_name -> aibridged.TrackTokenUsageRequest.OtherEntry - 0, // 1: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest - 2, // 2: aibridged.AIBridgeDaemon.TrackUserPrompt:input_type -> aibridged.TrackUserPromptRequest - 4, // 3: aibridged.AIBridgeDaemon.TrackToolUsage:input_type -> aibridged.TrackToolUsageRequest - 1, // 4: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse - 3, // 5: aibridged.AIBridgeDaemon.TrackUserPrompt:output_type -> aibridged.TrackUserPromptResponse - 5, // 6: aibridged.AIBridgeDaemon.TrackToolUsage:output_type -> aibridged.TrackToolUsageResponse - 4, // [4:7] is the sub-list for method output_type - 1, // [1:4] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 8, // 0: aibridged.TrackTokenUsageRequest.metadata:type_name -> aibridged.TrackTokenUsageRequest.MetadataEntry + 9, // 1: aibridged.TrackUserPromptRequest.metadata:type_name -> aibridged.TrackUserPromptRequest.MetadataEntry + 10, // 2: aibridged.TrackToolUsageRequest.metadata:type_name -> aibridged.TrackToolUsageRequest.MetadataEntry + 11, // 3: aibridged.TrackTokenUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any + 11, // 4: aibridged.TrackUserPromptRequest.MetadataEntry.value:type_name -> google.protobuf.Any + 11, // 5: aibridged.TrackToolUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any + 0, // 6: aibridged.AIBridgeDaemon.StartSession:input_type -> aibridged.StartSessionRequest + 2, // 7: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest + 4, // 8: aibridged.AIBridgeDaemon.TrackUserPrompt:input_type -> aibridged.TrackUserPromptRequest + 6, // 9: aibridged.AIBridgeDaemon.TrackToolUsage:input_type -> aibridged.TrackToolUsageRequest + 1, // 10: aibridged.AIBridgeDaemon.StartSession:output_type -> aibridged.StartSessionResponse + 3, // 11: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse + 5, // 12: aibridged.AIBridgeDaemon.TrackUserPrompt:output_type -> aibridged.TrackUserPromptResponse + 7, // 13: aibridged.AIBridgeDaemon.TrackToolUsage:output_type -> aibridged.TrackToolUsageResponse + 10, // [10:14] is the sub-list for method output_type + 6, // [6:10] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_aibridged_proto_aibridged_proto_init() } @@ -495,7 +645,7 @@ func file_aibridged_proto_aibridged_proto_init() { } if !protoimpl.UnsafeEnabled { file_aibridged_proto_aibridged_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackTokenUsageRequest); i { + switch v := v.(*StartSessionRequest); i { case 0: return &v.state case 1: @@ -507,7 +657,7 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackTokenUsageResponse); i { + switch v := v.(*StartSessionResponse); i { case 0: return &v.state case 1: @@ -519,7 +669,7 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackUserPromptRequest); i { + switch v := v.(*TrackTokenUsageRequest); i { case 0: return &v.state case 1: @@ -531,7 +681,7 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackUserPromptResponse); i { + switch v := v.(*TrackTokenUsageResponse); i { case 0: return &v.state case 1: @@ -543,7 +693,7 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackToolUsageRequest); i { + switch v := v.(*TrackUserPromptRequest); i { case 0: return &v.state case 1: @@ -555,6 +705,30 @@ func file_aibridged_proto_aibridged_proto_init() { } } file_aibridged_proto_aibridged_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TrackUserPromptResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_aibridged_proto_aibridged_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TrackToolUsageRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_aibridged_proto_aibridged_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TrackToolUsageResponse); i { case 0: return &v.state @@ -573,7 +747,7 @@ func file_aibridged_proto_aibridged_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_aibridged_proto_aibridged_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 11, NumExtensions: 0, NumServices: 1, }, diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index a3d9b0aca3f39..e5c43d33378cf 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -3,13 +3,24 @@ option go_package = "github.com/coder/coder/v2/aibridged/proto"; package aibridged; +import "google/protobuf/any.proto"; + +message StartSessionRequest { + string initiator_id = 1; + string provider = 2; + string model = 3; +} + +message StartSessionResponse { + string session_id = 1; +} + message TrackTokenUsageRequest { string session_id = 1; string msg_id = 2; int64 input_tokens = 3; int64 output_tokens = 4; - string model = 5; - map other = 6; + map metadata = 5; } message TrackTokenUsageResponse {} @@ -17,7 +28,7 @@ message TrackUserPromptRequest { string session_id = 1; string msg_id = 2; string prompt = 3; - string model = 4; + map metadata = 4; } message TrackUserPromptResponse {} @@ -25,14 +36,15 @@ message TrackToolUsageRequest { string session_id = 1; string msg_id = 2; string tool = 3; - string model = 4; - string input = 5; - bool injected = 6; + string input = 4; + bool injected = 5; + map metadata = 6; } message TrackToolUsageResponse {} // AIBridgeDaemon describes the service exposed by coderd, which allows the AI bridge to send data to coderd. service AIBridgeDaemon { + rpc StartSession(StartSessionRequest) returns (StartSessionResponse); rpc TrackTokenUsage(TrackTokenUsageRequest) returns (TrackTokenUsageResponse); rpc TrackUserPrompt(TrackUserPromptRequest) returns (TrackUserPromptResponse); rpc TrackToolUsage(TrackToolUsageRequest) returns (TrackToolUsageResponse); diff --git a/aibridged/proto/aibridged_drpc.pb.go b/aibridged/proto/aibridged_drpc.pb.go index dd38e9a007aef..f04dfe4f1c05d 100644 --- a/aibridged/proto/aibridged_drpc.pb.go +++ b/aibridged/proto/aibridged_drpc.pb.go @@ -38,6 +38,7 @@ func (drpcEncoding_File_aibridged_proto_aibridged_proto) JSONUnmarshal(buf []byt type DRPCAIBridgeDaemonClient interface { DRPCConn() drpc.Conn + StartSession(ctx context.Context, in *StartSessionRequest) (*StartSessionResponse, error) TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) TrackUserPrompt(ctx context.Context, in *TrackUserPromptRequest) (*TrackUserPromptResponse, error) TrackToolUsage(ctx context.Context, in *TrackToolUsageRequest) (*TrackToolUsageResponse, error) @@ -53,6 +54,15 @@ func NewDRPCAIBridgeDaemonClient(cc drpc.Conn) DRPCAIBridgeDaemonClient { func (c *drpcAIBridgeDaemonClient) DRPCConn() drpc.Conn { return c.cc } +func (c *drpcAIBridgeDaemonClient) StartSession(ctx context.Context, in *StartSessionRequest) (*StartSessionResponse, error) { + out := new(StartSessionResponse) + err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/StartSession", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + func (c *drpcAIBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) { out := new(TrackTokenUsageResponse) err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackTokenUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) @@ -81,6 +91,7 @@ func (c *drpcAIBridgeDaemonClient) TrackToolUsage(ctx context.Context, in *Track } type DRPCAIBridgeDaemonServer interface { + StartSession(context.Context, *StartSessionRequest) (*StartSessionResponse, error) TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) TrackUserPrompt(context.Context, *TrackUserPromptRequest) (*TrackUserPromptResponse, error) TrackToolUsage(context.Context, *TrackToolUsageRequest) (*TrackToolUsageResponse, error) @@ -88,6 +99,10 @@ type DRPCAIBridgeDaemonServer interface { type DRPCAIBridgeDaemonUnimplementedServer struct{} +func (s *DRPCAIBridgeDaemonUnimplementedServer) StartSession(context.Context, *StartSessionRequest) (*StartSessionResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) { return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } @@ -102,11 +117,20 @@ func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackToolUsage(context.Context, type DRPCAIBridgeDaemonDescription struct{} -func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 3 } +func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 4 } func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { case 0: + return "/aibridged.AIBridgeDaemon/StartSession", drpcEncoding_File_aibridged_proto_aibridged_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAIBridgeDaemonServer). + StartSession( + ctx, + in1.(*StartSessionRequest), + ) + }, DRPCAIBridgeDaemonServer.StartSession, true + case 1: return "/aibridged.AIBridgeDaemon/TrackTokenUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAIBridgeDaemonServer). @@ -115,7 +139,7 @@ func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc. in1.(*TrackTokenUsageRequest), ) }, DRPCAIBridgeDaemonServer.TrackTokenUsage, true - case 1: + case 2: return "/aibridged.AIBridgeDaemon/TrackUserPrompt", drpcEncoding_File_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAIBridgeDaemonServer). @@ -124,7 +148,7 @@ func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc. in1.(*TrackUserPromptRequest), ) }, DRPCAIBridgeDaemonServer.TrackUserPrompt, true - case 2: + case 3: return "/aibridged.AIBridgeDaemon/TrackToolUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAIBridgeDaemonServer). @@ -142,6 +166,22 @@ func DRPCRegisterAIBridgeDaemon(mux drpc.Mux, impl DRPCAIBridgeDaemonServer) err return mux.Register(impl, DRPCAIBridgeDaemonDescription{}) } +type DRPCAIBridgeDaemon_StartSessionStream interface { + drpc.Stream + SendAndClose(*StartSessionResponse) error +} + +type drpcAIBridgeDaemon_StartSessionStream struct { + drpc.Stream +} + +func (x *drpcAIBridgeDaemon_StartSessionStream) SendAndClose(m *StartSessionResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { + return err + } + return x.CloseSend() +} + type DRPCAIBridgeDaemon_TrackTokenUsageStream interface { drpc.Stream SendAndClose(*TrackTokenUsageResponse) error diff --git a/aibridged/session.go b/aibridged/session.go index 799a6bc14850e..40634ccf72523 100644 --- a/aibridged/session.go +++ b/aibridged/session.go @@ -12,7 +12,7 @@ type Model struct { // Session describes a (potentially) stateful interaction with an AI provider. type Session interface { - Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) (id string) + Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) LastUserPrompt() (*string, error) Model() Model ProcessRequest(w http.ResponseWriter, r *http.Request) error diff --git a/aibridged/session_anthropic_messages_base.go b/aibridged/session_anthropic_messages_base.go index af12c10f3acb3..ed750cf364ca3 100644 --- a/aibridged/session_anthropic_messages_base.go +++ b/aibridged/session_anthropic_messages_base.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/anthropics/anthropic-sdk-go" - "github.com/google/uuid" "golang.org/x/xerrors" "cdr.dev/slog" @@ -22,8 +21,8 @@ type AnthropicMessagesSessionBase struct { toolMgr ToolManager } -func (s *AnthropicMessagesSessionBase) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { - s.id = uuid.NewString() +func (s *AnthropicMessagesSessionBase) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { + s.id = id s.logger = logger.With(slog.F("session_id", s.id)) @@ -32,8 +31,6 @@ func (s *AnthropicMessagesSessionBase) Init(logger slog.Logger, baseURL, key str s.tracker = tracker s.toolMgr = toolMgr - - return s.id } func (s *AnthropicMessagesSessionBase) LastUserPrompt() (*string, error) { diff --git a/aibridged/session_anthropic_messages_blocking.go b/aibridged/session_anthropic_messages_blocking.go index e89ba3285a8d4..626cac186ac7e 100644 --- a/aibridged/session_anthropic_messages_blocking.go +++ b/aibridged/session_anthropic_messages_blocking.go @@ -27,8 +27,8 @@ func NewAnthropicMessagesBlockingSession(req *BetaMessageNewParamsWrapper) *Anth }} } -func (s *AnthropicMessagesBlockingSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { - return s.AnthropicMessagesSessionBase.Init(logger.Named("blocking"), baseURL, key, tracker, toolMgr) +func (s *AnthropicMessagesBlockingSession) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { + s.AnthropicMessagesSessionBase.Init(id, logger.Named("blocking"), baseURL, key, tracker, toolMgr) } func (s *AnthropicMessagesBlockingSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { diff --git a/aibridged/session_anthropic_messages_streaming.go b/aibridged/session_anthropic_messages_streaming.go index ade8c9bbcd696..1c134c0c83a8f 100644 --- a/aibridged/session_anthropic_messages_streaming.go +++ b/aibridged/session_anthropic_messages_streaming.go @@ -29,8 +29,8 @@ func NewAnthropicMessagesStreamingSession(req *BetaMessageNewParamsWrapper) *Ant }} } -func (s *AnthropicMessagesStreamingSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { - return s.AnthropicMessagesSessionBase.Init(logger.Named("streaming"), baseURL, key, tracker, toolMgr) +func (s *AnthropicMessagesStreamingSession) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { + s.AnthropicMessagesSessionBase.Init(id, logger.Named("streaming"), baseURL, key, tracker, toolMgr) } func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { diff --git a/aibridged/session_openai_chat_base.go b/aibridged/session_openai_chat_base.go index abc0ccb1d7c68..7f333015d01c7 100644 --- a/aibridged/session_openai_chat_base.go +++ b/aibridged/session_openai_chat_base.go @@ -1,7 +1,6 @@ package aibridged import ( - "github.com/google/uuid" "github.com/openai/openai-go" "golang.org/x/xerrors" @@ -20,8 +19,8 @@ type OpenAIChatSessionBase struct { toolMgr ToolManager } -func (s *OpenAIChatSessionBase) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { - s.id = uuid.NewString() +func (s *OpenAIChatSessionBase) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { + s.id = id s.logger = logger.With(slog.F("session_id", s.id)) @@ -30,8 +29,6 @@ func (s *OpenAIChatSessionBase) Init(logger slog.Logger, baseURL, key string, tr s.tracker = tracker s.toolMgr = toolMgr - - return s.id } func (s *OpenAIChatSessionBase) LastUserPrompt() (*string, error) { diff --git a/aibridged/session_openai_chat_blocking.go b/aibridged/session_openai_chat_blocking.go index babe6016edefd..0dc14e3392de5 100644 --- a/aibridged/session_openai_chat_blocking.go +++ b/aibridged/session_openai_chat_blocking.go @@ -25,8 +25,8 @@ func NewOpenAIBlockingChatSession(req *ChatCompletionNewParamsWrapper) *OpenAIBl }} } -func (s *OpenAIBlockingChatSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { - return s.OpenAIChatSessionBase.Init(logger.Named("streaming"), baseURL, key, tracker, toolMgr) +func (s *OpenAIBlockingChatSession) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { + s.OpenAIChatSessionBase.Init(id, logger.Named("streaming"), baseURL, key, tracker, toolMgr) } func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { diff --git a/aibridged/session_openai_chat_streaming.go b/aibridged/session_openai_chat_streaming.go index cf26901935ce2..847d8988826c3 100644 --- a/aibridged/session_openai_chat_streaming.go +++ b/aibridged/session_openai_chat_streaming.go @@ -27,8 +27,8 @@ func NewOpenAIStreamingChatSession(req *ChatCompletionNewParamsWrapper) *OpenAIS }} } -func (s *OpenAIStreamingChatSession) Init(logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) string { - return s.OpenAIChatSessionBase.Init(logger.Named("streaming"), baseURL, key, tracker, toolMgr) +func (s *OpenAIStreamingChatSession) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { + s.OpenAIChatSessionBase.Init(id, logger.Named("streaming"), baseURL, key, tracker, toolMgr) } func (s *OpenAIStreamingChatSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { diff --git a/aibridged/tracker.go b/aibridged/tracker.go index c2bed978d3b40..206cb902e0584 100644 --- a/aibridged/tracker.go +++ b/aibridged/tracker.go @@ -3,15 +3,31 @@ package aibridged import ( "context" "encoding/json" - "fmt" "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/structpb" "github.com/coder/coder/v2/aibridged/proto" ) type Metadata map[string]any +func (m Metadata) MarshalForProto() map[string]*anypb.Any { + if len(m) == 0 { + return nil + } + out := make(map[string]*anypb.Any, len(m)) + for k, v := range m { + if sv, err := structpb.NewValue(v); err == nil { + if av, err := anypb.New(sv); err == nil { + out[k] = av + } + } + } + return out +} + type Tracker interface { TrackTokensUsage(ctx context.Context, sessionID, msgID string, model Model, promptTokens, completionTokens int64, metadata Metadata) error TrackPromptUsage(ctx context.Context, sessionID, msgID string, model Model, prompt string, metadata Metadata) error @@ -35,8 +51,7 @@ func (d *DRPCTracker) TrackTokensUsage(ctx context.Context, sessionID, msgID str MsgId: msgID, InputTokens: promptTokens, OutputTokens: completionTokens, - Model: fmt.Sprintf("%s.%s", model.Provider, model.ModelName), // TODO: make first-class type in proto. - // Other: metadata, // TODO: implement map in proto. + Metadata: metadata.MarshalForProto(), }) return err } @@ -46,7 +61,7 @@ func (d *DRPCTracker) TrackPromptUsage(ctx context.Context, sessionID, msgID str SessionId: sessionID, MsgId: msgID, Prompt: prompt, - Model: fmt.Sprintf("%s.%s", model.Provider, model.ModelName), // TODO: make first-class type in proto. + Metadata: metadata.MarshalForProto(), }) return err } @@ -73,9 +88,9 @@ func (d *DRPCTracker) TrackToolUsage(ctx context.Context, sessionID, msgID strin SessionId: sessionID, MsgId: msgID, Tool: name, - Model: fmt.Sprintf("%s.%s", model.Provider, model.ModelName), // TODO: make first-class type in proto. Input: string(serialized), Injected: injected, + Metadata: metadata.MarshalForProto(), }) return err } diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index 759550a12f8e8..8a0a4bd3178d1 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -4,65 +4,130 @@ import ( "context" "encoding/json" + "github.com/google/uuid" "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/structpb" + + "cdr.dev/slog" "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/coderd/database" ) +var _ proto.DRPCAIBridgeDaemonServer = &Server{} + type Server struct { // lifecycleCtx must be tied to the API server's lifecycle // as when the API server shuts down, we want to cancel any // long-running operations. lifecycleCtx context.Context store database.Store + logger slog.Logger } -func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { - raw, err := json.Marshal(in) +func NewServer(lifecycleCtx context.Context, store database.Store, logger slog.Logger) (*Server, error) { + return &Server{ + lifecycleCtx: lifecycleCtx, + store: store, + logger: logger.Named("aibridgedserver"), + }, nil +} + +// StartSession implements proto.DRPCAIBridgeDaemonServer. +func (s *Server) StartSession(ctx context.Context, in *proto.StartSessionRequest) (*proto.StartSessionResponse, error) { + initID, err := uuid.Parse(in.GetInitiatorId()) + if err != nil { + return nil, xerrors.Errorf("invalid initiator ID %q: %w", in.GetInitiatorId(), err) + } + + id, err := s.store.InsertAIBridgeSession(ctx, database.InsertAIBridgeSessionParams{ + ID: uuid.New(), + InitiatorID: initID, + Provider: in.Provider, + Model: in.Model, + }) if err != nil { - return nil, xerrors.Errorf("marshal event: %w", err) + return nil, xerrors.Errorf("start session: %w", err) } - err = s.store.InsertWormholeEvent(ctx, database.InsertWormholeEventParams{Event: raw, EventType: "token_usage"}) + return &proto.StartSessionResponse{SessionId: id.String()}, nil +} + +func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { + sessID, err := uuid.Parse(in.GetSessionId()) if err != nil { - return nil, xerrors.Errorf("store event: %w", err) + return nil, xerrors.Errorf("failed to parse session_id %q: %w", in.GetSessionId(), err) } + err = s.store.InsertAIBridgeTokenUsage(ctx, database.InsertAIBridgeTokenUsageParams{ + ID: uuid.New(), + SessionID: sessID, + ProviderID: in.GetMsgId(), + InputTokens: in.GetInputTokens(), + OutputTokens: in.GetOutputTokens(), + Metadata: s.marshalMetadata(in.GetMetadata()), + }) + if err != nil { + return nil, xerrors.Errorf("insert token usage: %w", err) + } return &proto.TrackTokenUsageResponse{}, nil } func (s *Server) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptRequest) (*proto.TrackUserPromptResponse, error) { - raw, err := json.Marshal(in) + sessID, err := uuid.Parse(in.GetSessionId()) if err != nil { - return nil, xerrors.Errorf("marshal event: %w", err) + return nil, xerrors.Errorf("failed to parse session_id %q: %w", in.GetSessionId(), err) } - err = s.store.InsertWormholeEvent(ctx, database.InsertWormholeEventParams{Event: raw, EventType: "user_prompt"}) + err = s.store.InsertAIBridgeUserPrompt(ctx, database.InsertAIBridgeUserPromptParams{ + ID: uuid.New(), + SessionID: sessID, + ProviderID: in.GetMsgId(), + Prompt: in.GetPrompt(), + Metadata: s.marshalMetadata(in.GetMetadata()), + }) if err != nil { - return nil, xerrors.Errorf("store event: %w", err) + return nil, xerrors.Errorf("insert user prompt: %w", err) } - return &proto.TrackUserPromptResponse{}, nil } func (s *Server) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageRequest) (*proto.TrackToolUsageResponse, error) { - raw, err := json.Marshal(in) + sessID, err := uuid.Parse(in.GetSessionId()) if err != nil { - return nil, xerrors.Errorf("marshal event: %w", err) + return nil, xerrors.Errorf("failed to parse session_id %q: %w", in.GetSessionId(), err) } - err = s.store.InsertWormholeEvent(ctx, database.InsertWormholeEventParams{Event: raw, EventType: "tool_usage"}) + err = s.store.InsertAIBridgeToolUsage(ctx, database.InsertAIBridgeToolUsageParams{ + ID: uuid.New(), + SessionID: sessID, + ProviderID: in.GetMsgId(), + Tool: in.GetTool(), + Input: in.GetInput(), + Injected: in.GetInjected(), + Metadata: s.marshalMetadata(in.GetMetadata()), + }) if err != nil { - return nil, xerrors.Errorf("store event: %w", err) + return nil, xerrors.Errorf("insert tool usage: %w", err) } - return &proto.TrackToolUsageResponse{}, nil } -func NewServer(lifecycleCtx context.Context, store database.Store) (*Server, error) { - return &Server{ - lifecycleCtx: lifecycleCtx, - store: store, - }, nil +func (s *Server) marshalMetadata(in map[string]*anypb.Any) []byte { + mdMap := map[string]interface{}{} + for k, v := range in { + if v == nil { + continue + } + var sv structpb.Value + if err := v.UnmarshalTo(&sv); err == nil { + mdMap[k] = sv.AsInterface() + } + } + out, err := json.Marshal(mdMap) + if err != nil { + s.logger.Warn(s.lifecycleCtx, "failed to marshal metadata", slog.Error(err)) + } + return out } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d5d39a0208002..0920ef42fef4e 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10934,16 +10934,38 @@ const docTemplate = `{ } } }, - "codersdk.AIBridgeConfig": { + "codersdk.AIBridgeAnthropicConfig": { "type": "object", "properties": { - "anthropic_base_url": { + "base_url": { "type": "string" }, + "key": { + "type": "string" + } + } + }, + "codersdk.AIBridgeConfig": { + "type": "object", + "properties": { + "anthropic": { + "$ref": "#/definitions/codersdk.AIBridgeAnthropicConfig" + }, "daemons": { "type": "integer" }, - "openai_base_url": { + "openai": { + "$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig" + } + } + }, + "codersdk.AIBridgeOpenAIConfig": { + "type": "object", + "properties": { + "base_url": { + "type": "string" + }, + "key": { "type": "string" } } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 4f36d99597457..e92d7600412c7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9694,16 +9694,38 @@ } } }, - "codersdk.AIBridgeConfig": { + "codersdk.AIBridgeAnthropicConfig": { "type": "object", "properties": { - "anthropic_base_url": { + "base_url": { "type": "string" }, + "key": { + "type": "string" + } + } + }, + "codersdk.AIBridgeConfig": { + "type": "object", + "properties": { + "anthropic": { + "$ref": "#/definitions/codersdk.AIBridgeAnthropicConfig" + }, "daemons": { "type": "integer" }, - "openai_base_url": { + "openai": { + "$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig" + } + } + }, + "codersdk.AIBridgeOpenAIConfig": { + "type": "object", + "properties": { + "base_url": { + "type": "string" + }, + "key": { "type": "string" } } diff --git a/coderd/coderd.go b/coderd/coderd.go index 2bce2dd99ec04..d3f7dab1880fa 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1982,7 +1982,7 @@ func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name strin mux := drpcmux.New() api.Logger.Debug(dialCtx, "starting in-memory AI bridge daemon", slog.F("name", name)) logger := api.Logger.Named(fmt.Sprintf("inmem-aibridged-%s", name)) - srv, err := aibridgedserver.NewServer(api.ctx, api.Database) + srv, err := aibridgedserver.NewServer(api.ctx, api.Database, api.Logger) if err != nil { return nil, err } diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 0f49085cc3638..a780c41a6c3e0 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3617,6 +3617,26 @@ func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now ti return q.db.GetWorkspacesEligibleForTransition(ctx, now) } +func (q *querier) InsertAIBridgeSession(ctx context.Context, arg database.InsertAIBridgeSessionParams) (uuid.UUID, error) { + // TODO: authz. + return q.db.InsertAIBridgeSession(ctx, arg) +} + +func (q *querier) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error { + // TODO: authz. + return q.db.InsertAIBridgeTokenUsage(ctx, arg) +} + +func (q *querier) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error { + // TODO: authz. + return q.db.InsertAIBridgeToolUsage(ctx, arg) +} + +func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error { + // TODO: authz. + return q.db.InsertAIBridgeUserPrompt(ctx, arg) +} + func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { return insert(q.log, q.auth, rbac.ResourceApiKey.WithOwner(arg.UserID.String()), @@ -4145,10 +4165,6 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab return q.db.InsertWorkspaceResourceMetadata(ctx, arg) } -func (q *querier) InsertWormholeEvent(ctx context.Context, arg database.InsertWormholeEventParams) error { - return q.db.InsertWormholeEvent(ctx, arg) // TODO: authz. -} - func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganization)(ctx, organizationID) } diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 9d1a9222d9bdc..8c35bb817bf26 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2098,6 +2098,34 @@ func (m queryMetricsStore) GetWorkspacesEligibleForTransition(ctx context.Contex return workspaces, err } +func (m queryMetricsStore) InsertAIBridgeSession(ctx context.Context, arg database.InsertAIBridgeSessionParams) (uuid.UUID, error) { + start := time.Now() + r0, r1 := m.s.InsertAIBridgeSession(ctx, arg) + m.queryLatencies.WithLabelValues("InsertAIBridgeSession").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error { + start := time.Now() + r0 := m.s.InsertAIBridgeTokenUsage(ctx, arg) + m.queryLatencies.WithLabelValues("InsertAIBridgeTokenUsage").Observe(time.Since(start).Seconds()) + return r0 +} + +func (m queryMetricsStore) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error { + start := time.Now() + r0 := m.s.InsertAIBridgeToolUsage(ctx, arg) + m.queryLatencies.WithLabelValues("InsertAIBridgeToolUsage").Observe(time.Since(start).Seconds()) + return r0 +} + +func (m queryMetricsStore) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error { + start := time.Now() + r0 := m.s.InsertAIBridgeUserPrompt(ctx, arg) + m.queryLatencies.WithLabelValues("InsertAIBridgeUserPrompt").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { start := time.Now() key, err := m.s.InsertAPIKey(ctx, arg) @@ -2525,13 +2553,6 @@ func (m queryMetricsStore) InsertWorkspaceResourceMetadata(ctx context.Context, return metadata, err } -func (m queryMetricsStore) InsertWormholeEvent(ctx context.Context, arg database.InsertWormholeEventParams) error { - start := time.Now() - r0 := m.s.InsertWormholeEvent(ctx, arg) - m.queryLatencies.WithLabelValues("InsertWormholeEvent").Observe(time.Since(start).Seconds()) - return r0 -} - func (m queryMetricsStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { start := time.Now() r0, r1 := m.s.ListProvisionerKeysByOrganization(ctx, organizationID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 4b0ca2312031e..ef9aaae2a40f1 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -4486,6 +4486,63 @@ func (mr *MockStoreMockRecorder) InTx(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InTx", reflect.TypeOf((*MockStore)(nil).InTx), arg0, arg1) } +// InsertAIBridgeSession mocks base method. +func (m *MockStore) InsertAIBridgeSession(ctx context.Context, arg database.InsertAIBridgeSessionParams) (uuid.UUID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertAIBridgeSession", ctx, arg) + ret0, _ := ret[0].(uuid.UUID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertAIBridgeSession indicates an expected call of InsertAIBridgeSession. +func (mr *MockStoreMockRecorder) InsertAIBridgeSession(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeSession", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeSession), ctx, arg) +} + +// InsertAIBridgeTokenUsage mocks base method. +func (m *MockStore) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertAIBridgeTokenUsage", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertAIBridgeTokenUsage indicates an expected call of InsertAIBridgeTokenUsage. +func (mr *MockStoreMockRecorder) InsertAIBridgeTokenUsage(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeTokenUsage", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeTokenUsage), ctx, arg) +} + +// InsertAIBridgeToolUsage mocks base method. +func (m *MockStore) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertAIBridgeToolUsage", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertAIBridgeToolUsage indicates an expected call of InsertAIBridgeToolUsage. +func (mr *MockStoreMockRecorder) InsertAIBridgeToolUsage(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeToolUsage", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeToolUsage), ctx, arg) +} + +// InsertAIBridgeUserPrompt mocks base method. +func (m *MockStore) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertAIBridgeUserPrompt", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertAIBridgeUserPrompt indicates an expected call of InsertAIBridgeUserPrompt. +func (mr *MockStoreMockRecorder) InsertAIBridgeUserPrompt(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeUserPrompt", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeUserPrompt), ctx, arg) +} + // InsertAPIKey mocks base method. func (m *MockStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { m.ctrl.T.Helper() @@ -5387,20 +5444,6 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(ctx, arg any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResourceMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResourceMetadata), ctx, arg) } -// InsertWormholeEvent mocks base method. -func (m *MockStore) InsertWormholeEvent(ctx context.Context, arg database.InsertWormholeEventParams) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertWormholeEvent", ctx, arg) - ret0, _ := ret[0].(error) - return ret0 -} - -// InsertWormholeEvent indicates an expected call of InsertWormholeEvent. -func (mr *MockStoreMockRecorder) InsertWormholeEvent(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWormholeEvent", reflect.TypeOf((*MockStore)(nil).InsertWormholeEvent), ctx, arg) -} - // ListProvisionerKeysByOrganization mocks base method. func (m *MockStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 043eaf94bc8c1..519a28ee0a1f1 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -814,6 +814,44 @@ BEGIN END; $$; +CREATE TABLE aibridge_sessions ( + id uuid NOT NULL, + initiator_id uuid NOT NULL, + provider text NOT NULL, + model text NOT NULL, + created_at timestamp with time zone DEFAULT now() +); + +CREATE TABLE aibridge_token_usages ( + id uuid NOT NULL, + session_id uuid NOT NULL, + provider_id text NOT NULL, + input_tokens bigint NOT NULL, + output_tokens bigint NOT NULL, + metadata jsonb, + created_at timestamp with time zone DEFAULT now() +); + +CREATE TABLE aibridge_tool_usages ( + id uuid NOT NULL, + session_id uuid NOT NULL, + provider_id text NOT NULL, + tool text NOT NULL, + input text NOT NULL, + injected boolean DEFAULT false NOT NULL, + metadata jsonb, + created_at timestamp with time zone DEFAULT now() +); + +CREATE TABLE aibridge_user_prompts ( + id uuid NOT NULL, + session_id uuid NOT NULL, + provider_id text NOT NULL, + prompt text NOT NULL, + metadata jsonb, + created_at timestamp with time zone DEFAULT now() +); + CREATE TABLE api_keys ( id text NOT NULL, hashed_secret bytea NOT NULL, @@ -2471,13 +2509,6 @@ CREATE VIEW workspaces_expanded AS COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; -CREATE TABLE wormhole ( - id uuid, - created_at timestamp with time zone NOT NULL, - event jsonb NOT NULL, - event_type character varying(32) NOT NULL -); - ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass); ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provisioner_job_logs_id_seq'::regclass); @@ -2493,6 +2524,18 @@ ALTER TABLE ONLY workspace_resource_metadata ALTER COLUMN id SET DEFAULT nextval ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id); +ALTER TABLE ONLY aibridge_sessions + ADD CONSTRAINT aibridge_sessions_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY aibridge_token_usages + ADD CONSTRAINT aibridge_token_usages_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY aibridge_tool_usages + ADD CONSTRAINT aibridge_tool_usages_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY aibridge_user_prompts + ADD CONSTRAINT aibridge_user_prompts_pkey PRIMARY KEY (id); + ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id); @@ -2776,6 +2819,24 @@ CREATE INDEX idx_agent_stats_created_at ON workspace_agent_stats USING btree (cr CREATE INDEX idx_agent_stats_user_id ON workspace_agent_stats USING btree (user_id); +CREATE INDEX idx_aibridge_sessions_model ON aibridge_sessions USING btree (model); + +CREATE INDEX idx_aibridge_sessions_provider ON aibridge_sessions USING btree (provider); + +CREATE INDEX idx_aibridge_token_usages_session_id ON aibridge_token_usages USING btree (session_id); + +CREATE INDEX idx_aibridge_token_usages_session_provider_id ON aibridge_token_usages USING btree (session_id, provider_id); + +CREATE INDEX idx_aibridge_tool_usages_session_id ON aibridge_tool_usages USING btree (session_id); + +CREATE INDEX idx_aibridge_tool_usages_session_provider_id ON aibridge_tool_usages USING btree (session_id, provider_id); + +CREATE INDEX idx_aibridge_tool_usages_tool ON aibridge_tool_usages USING btree (tool); + +CREATE INDEX idx_aibridge_user_prompts_session_id ON aibridge_user_prompts USING btree (session_id); + +CREATE INDEX idx_aibridge_user_prompts_session_provider_id ON aibridge_user_prompts USING btree (session_id, provider_id); + CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type); CREATE INDEX idx_api_keys_user ON api_keys USING btree (user_id); @@ -3004,6 +3065,18 @@ COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS 'U the uniqueness requirement. A trigger allows us to enforce uniqueness going forward without requiring a migration to clean up historical data.'; +ALTER TABLE ONLY aibridge_sessions + ADD CONSTRAINT aibridge_sessions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id) ON DELETE CASCADE; + +ALTER TABLE ONLY aibridge_token_usages + ADD CONSTRAINT aibridge_token_usages_session_id_fkey FOREIGN KEY (session_id) REFERENCES aibridge_sessions(id) ON DELETE CASCADE; + +ALTER TABLE ONLY aibridge_tool_usages + ADD CONSTRAINT aibridge_tool_usages_session_id_fkey FOREIGN KEY (session_id) REFERENCES aibridge_sessions(id) ON DELETE CASCADE; + +ALTER TABLE ONLY aibridge_user_prompts + ADD CONSTRAINT aibridge_user_prompts_session_id_fkey FOREIGN KEY (session_id) REFERENCES aibridge_sessions(id) ON DELETE CASCADE; + ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index c3aaf7342a97c..93e9747566f07 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -6,6 +6,10 @@ type ForeignKeyConstraint string // ForeignKeyConstraint enums. const ( + ForeignKeyAibridgeSessionsInitiatorID ForeignKeyConstraint = "aibridge_sessions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_sessions ADD CONSTRAINT aibridge_sessions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyAibridgeTokenUsagesSessionID ForeignKeyConstraint = "aibridge_token_usages_session_id_fkey" // ALTER TABLE ONLY aibridge_token_usages ADD CONSTRAINT aibridge_token_usages_session_id_fkey FOREIGN KEY (session_id) REFERENCES aibridge_sessions(id) ON DELETE CASCADE; + ForeignKeyAibridgeToolUsagesSessionID ForeignKeyConstraint = "aibridge_tool_usages_session_id_fkey" // ALTER TABLE ONLY aibridge_tool_usages ADD CONSTRAINT aibridge_tool_usages_session_id_fkey FOREIGN KEY (session_id) REFERENCES aibridge_sessions(id) ON DELETE CASCADE; + ForeignKeyAibridgeUserPromptsSessionID ForeignKeyConstraint = "aibridge_user_prompts_session_id_fkey" // ALTER TABLE ONLY aibridge_user_prompts ADD CONSTRAINT aibridge_user_prompts_session_id_fkey FOREIGN KEY (session_id) REFERENCES aibridge_sessions(id) ON DELETE CASCADE; ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyConnectionLogsOrganizationID ForeignKeyConstraint = "connection_logs_organization_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ForeignKeyConnectionLogsWorkspaceID ForeignKeyConstraint = "connection_logs_workspace_id_fkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000357_aibridge.down.sql b/coderd/database/migrations/000357_aibridge.down.sql new file mode 100644 index 0000000000000..ef4b81bd08b0c --- /dev/null +++ b/coderd/database/migrations/000357_aibridge.down.sql @@ -0,0 +1,4 @@ +DROP TABLE IF EXISTS aibridge_tool_usages CASCADE; +DROP TABLE IF EXISTS aibridge_user_prompts CASCADE; +DROP TABLE IF EXISTS aibridge_token_usages CASCADE; +DROP TABLE IF EXISTS aibridge_sessions CASCADE; diff --git a/coderd/database/migrations/000357_aibridge.up.sql b/coderd/database/migrations/000357_aibridge.up.sql index 0dfe95ef0d658..71362d7d79dfd 100644 --- a/coderd/database/migrations/000357_aibridge.up.sql +++ b/coderd/database/migrations/000357_aibridge.up.sql @@ -1,7 +1,50 @@ -CREATE TABLE wormhole -( - id UUID, - created_at timestamptz NOT NULL, - event jsonb NOT NULL, - event_type varchar(32) NOT NULL +CREATE TABLE IF NOT EXISTS aibridge_sessions ( + id UUID PRIMARY KEY, + initiator_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + provider TEXT NOT NULL, + model TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); + +CREATE INDEX idx_aibridge_sessions_provider ON aibridge_sessions(provider); +CREATE INDEX idx_aibridge_sessions_model ON aibridge_sessions(model); + +CREATE TABLE IF NOT EXISTS aibridge_token_usages ( + id UUID PRIMARY KEY, + session_id UUID NOT NULL REFERENCES aibridge_sessions(id) ON DELETE CASCADE, + provider_id TEXT NOT NULL, + input_tokens BIGINT NOT NULL, + output_tokens BIGINT NOT NULL, + metadata JSONB DEFAULT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_aibridge_token_usages_session_id ON aibridge_token_usages(session_id); +CREATE INDEX idx_aibridge_token_usages_session_provider_id ON aibridge_token_usages(session_id, provider_id); + +CREATE TABLE IF NOT EXISTS aibridge_user_prompts ( + id UUID PRIMARY KEY, + session_id UUID NOT NULL REFERENCES aibridge_sessions(id) ON DELETE CASCADE, + provider_id TEXT NOT NULL, + prompt TEXT NOT NULL, + metadata JSONB DEFAULT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_aibridge_user_prompts_session_id ON aibridge_user_prompts(session_id); +CREATE INDEX idx_aibridge_user_prompts_session_provider_id ON aibridge_user_prompts(session_id, provider_id); + +CREATE TABLE IF NOT EXISTS aibridge_tool_usages ( + id UUID PRIMARY KEY, + session_id UUID NOT NULL REFERENCES aibridge_sessions(id) ON DELETE CASCADE, + provider_id TEXT NOT NULL, + tool TEXT NOT NULL, + input TEXT NOT NULL, + injected BOOLEAN NOT NULL DEFAULT FALSE, + metadata JSONB DEFAULT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_aibridge_tool_usages_session_id ON aibridge_tool_usages(session_id); +CREATE INDEX idx_aibridge_tool_usages_tool ON aibridge_tool_usages(tool); +CREATE INDEX idx_aibridge_tool_usages_session_provider_id ON aibridge_tool_usages(session_id, provider_id); diff --git a/coderd/database/models.go b/coderd/database/models.go index 48e06ea52c01f..56ef6950df5ac 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2968,6 +2968,44 @@ type APIKey struct { TokenName string `db:"token_name" json:"token_name"` } +type AibridgeSession struct { + ID uuid.UUID `db:"id" json:"id"` + InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` + Provider string `db:"provider" json:"provider"` + Model string `db:"model" json:"model"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` +} + +type AibridgeTokenUsage struct { + ID uuid.UUID `db:"id" json:"id"` + SessionID uuid.UUID `db:"session_id" json:"session_id"` + ProviderID string `db:"provider_id" json:"provider_id"` + InputTokens int64 `db:"input_tokens" json:"input_tokens"` + OutputTokens int64 `db:"output_tokens" json:"output_tokens"` + Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` +} + +type AibridgeToolUsage struct { + ID uuid.UUID `db:"id" json:"id"` + SessionID uuid.UUID `db:"session_id" json:"session_id"` + ProviderID string `db:"provider_id" json:"provider_id"` + Tool string `db:"tool" json:"tool"` + Input string `db:"input" json:"input"` + Injected bool `db:"injected" json:"injected"` + Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` +} + +type AibridgeUserPrompt struct { + ID uuid.UUID `db:"id" json:"id"` + SessionID uuid.UUID `db:"session_id" json:"session_id"` + ProviderID string `db:"provider_id" json:"provider_id"` + Prompt string `db:"prompt" json:"prompt"` + Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` +} + type AuditLog struct { ID uuid.UUID `db:"id" json:"id"` Time time.Time `db:"time" json:"time"` @@ -4281,10 +4319,3 @@ type WorkspaceTable struct { GroupACL WorkspaceACL `db:"group_acl" json:"group_acl"` UserACL WorkspaceACL `db:"user_acl" json:"user_acl"` } - -type Wormhole struct { - ID uuid.NullUUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - Event json.RawMessage `db:"event" json:"event"` - EventType string `db:"event_type" json:"event_type"` -} diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d542c128e3110..63bd037603110 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -473,6 +473,10 @@ type sqlcQuerier interface { GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error) + InsertAIBridgeSession(ctx context.Context, arg InsertAIBridgeSessionParams) (uuid.UUID, error) + InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) error + InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) error + InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) error InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) // We use the organization_id as the id // for simplicity since all users is @@ -544,7 +548,6 @@ type sqlcQuerier interface { InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) - InsertWormholeEvent(ctx context.Context, arg InsertWormholeEventParams) error ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 6568b27699d31..2d9d363954f3b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -107,6 +107,118 @@ func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBump return err } +const insertAIBridgeSession = `-- name: InsertAIBridgeSession :one +INSERT INTO aibridge_sessions (id, initiator_id, provider, model) +VALUES ($1::uuid, $2::uuid, $3, $4) +RETURNING $1::uuid +` + +type InsertAIBridgeSessionParams struct { + ID uuid.UUID `db:"id" json:"id"` + InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` + Provider string `db:"provider" json:"provider"` + Model string `db:"model" json:"model"` +} + +func (q *sqlQuerier) InsertAIBridgeSession(ctx context.Context, arg InsertAIBridgeSessionParams) (uuid.UUID, error) { + row := q.db.QueryRowContext(ctx, insertAIBridgeSession, + arg.ID, + arg.InitiatorID, + arg.Provider, + arg.Model, + ) + var column_1 uuid.UUID + err := row.Scan(&column_1) + return column_1, err +} + +const insertAIBridgeTokenUsage = `-- name: InsertAIBridgeTokenUsage :exec +INSERT INTO aibridge_token_usages ( + id, session_id, provider_id, input_tokens, output_tokens, metadata +) VALUES ( + $1, $2, $3, $4, $5, COALESCE($6::jsonb, '{}'::jsonb) +) +` + +type InsertAIBridgeTokenUsageParams struct { + ID uuid.UUID `db:"id" json:"id"` + SessionID uuid.UUID `db:"session_id" json:"session_id"` + ProviderID string `db:"provider_id" json:"provider_id"` + InputTokens int64 `db:"input_tokens" json:"input_tokens"` + OutputTokens int64 `db:"output_tokens" json:"output_tokens"` + Metadata json.RawMessage `db:"metadata" json:"metadata"` +} + +func (q *sqlQuerier) InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) error { + _, err := q.db.ExecContext(ctx, insertAIBridgeTokenUsage, + arg.ID, + arg.SessionID, + arg.ProviderID, + arg.InputTokens, + arg.OutputTokens, + arg.Metadata, + ) + return err +} + +const insertAIBridgeToolUsage = `-- name: InsertAIBridgeToolUsage :exec +INSERT INTO aibridge_tool_usages ( + id, session_id, provider_id, tool, input, injected, metadata +) VALUES ( + $1, $2, $3, $4, $5, $6, COALESCE($7::jsonb, '{}'::jsonb) +) +` + +type InsertAIBridgeToolUsageParams struct { + ID uuid.UUID `db:"id" json:"id"` + SessionID uuid.UUID `db:"session_id" json:"session_id"` + ProviderID string `db:"provider_id" json:"provider_id"` + Tool string `db:"tool" json:"tool"` + Input string `db:"input" json:"input"` + Injected bool `db:"injected" json:"injected"` + Metadata json.RawMessage `db:"metadata" json:"metadata"` +} + +func (q *sqlQuerier) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) error { + _, err := q.db.ExecContext(ctx, insertAIBridgeToolUsage, + arg.ID, + arg.SessionID, + arg.ProviderID, + arg.Tool, + arg.Input, + arg.Injected, + arg.Metadata, + ) + return err +} + +const insertAIBridgeUserPrompt = `-- name: InsertAIBridgeUserPrompt :exec +INSERT INTO aibridge_user_prompts ( + id, session_id, provider_id, prompt, metadata +) VALUES ( + $1, $2, $3, $4, COALESCE($5::jsonb, '{}'::jsonb) +) +` + +type InsertAIBridgeUserPromptParams struct { + ID uuid.UUID `db:"id" json:"id"` + SessionID uuid.UUID `db:"session_id" json:"session_id"` + ProviderID string `db:"provider_id" json:"provider_id"` + Prompt string `db:"prompt" json:"prompt"` + Metadata json.RawMessage `db:"metadata" json:"metadata"` +} + +func (q *sqlQuerier) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) error { + _, err := q.db.ExecContext(ctx, insertAIBridgeUserPrompt, + arg.ID, + arg.SessionID, + arg.ProviderID, + arg.Prompt, + arg.Metadata, + ) + return err +} + const deleteAPIKeyByID = `-- name: DeleteAPIKeyByID :exec DELETE FROM api_keys @@ -21278,18 +21390,3 @@ func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg Insert } return items, nil } - -const insertWormholeEvent = `-- name: InsertWormholeEvent :exec -INSERT INTO wormhole (id, created_at, event, event_type) -VALUES (gen_random_uuid(), now(), $1, $2) -` - -type InsertWormholeEventParams struct { - Event json.RawMessage `db:"event" json:"event"` - EventType string `db:"event_type" json:"event_type"` -} - -func (q *sqlQuerier) InsertWormholeEvent(ctx context.Context, arg InsertWormholeEventParams) error { - _, err := q.db.ExecContext(ctx, insertWormholeEvent, arg.Event, arg.EventType) - return err -} diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql new file mode 100644 index 0000000000000..f8f30e88ae766 --- /dev/null +++ b/coderd/database/queries/aibridge.sql @@ -0,0 +1,25 @@ +-- name: InsertAIBridgeSession :one +INSERT INTO aibridge_sessions (id, initiator_id, provider, model) +VALUES (@id::uuid, @initiator_id::uuid, @provider, @model) +RETURNING @id::uuid; + +-- name: InsertAIBridgeTokenUsage :exec +INSERT INTO aibridge_token_usages ( + id, session_id, provider_id, input_tokens, output_tokens, metadata +) VALUES ( + @id, @session_id, @provider_id, @input_tokens, @output_tokens, COALESCE(@metadata::jsonb, '{}'::jsonb) +); + +-- name: InsertAIBridgeUserPrompt :exec +INSERT INTO aibridge_user_prompts ( + id, session_id, provider_id, prompt, metadata +) VALUES ( + @id, @session_id, @provider_id, @prompt, COALESCE(@metadata::jsonb, '{}'::jsonb) +); + +-- name: InsertAIBridgeToolUsage :exec +INSERT INTO aibridge_tool_usages ( + id, session_id, provider_id, tool, input, injected, metadata +) VALUES ( + @id, @session_id, @provider_id, @tool, @input, @injected, COALESCE(@metadata::jsonb, '{}'::jsonb) +); diff --git a/coderd/database/queries/wormhole.sql b/coderd/database/queries/wormhole.sql deleted file mode 100644 index 3f21bcbdff8f2..0000000000000 --- a/coderd/database/queries/wormhole.sql +++ /dev/null @@ -1,3 +0,0 @@ --- name: InsertWormholeEvent :exec -INSERT INTO wormhole (id, created_at, event, event_type) -VALUES (gen_random_uuid(), now(), @event, @event_type); diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 38c95e67410c9..bf3503f3b2c79 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -7,6 +7,10 @@ type UniqueConstraint string // UniqueConstraint enums. const ( UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id); + UniqueAibridgeSessionsPkey UniqueConstraint = "aibridge_sessions_pkey" // ALTER TABLE ONLY aibridge_sessions ADD CONSTRAINT aibridge_sessions_pkey PRIMARY KEY (id); + UniqueAibridgeTokenUsagesPkey UniqueConstraint = "aibridge_token_usages_pkey" // ALTER TABLE ONLY aibridge_token_usages ADD CONSTRAINT aibridge_token_usages_pkey PRIMARY KEY (id); + UniqueAibridgeToolUsagesPkey UniqueConstraint = "aibridge_tool_usages_pkey" // ALTER TABLE ONLY aibridge_tool_usages ADD CONSTRAINT aibridge_tool_usages_pkey PRIMARY KEY (id); + UniqueAibridgeUserPromptsPkey UniqueConstraint = "aibridge_user_prompts_pkey" // ALTER TABLE ONLY aibridge_user_prompts ADD CONSTRAINT aibridge_user_prompts_pkey PRIMARY KEY (id); UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id); UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id); UniqueConnectionLogsPkey UniqueConstraint = "connection_logs_pkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_pkey PRIMARY KEY (id); diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index ba9fe33d9c746..df1422eb9cf5b 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -163,9 +163,15 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "agent_stat_refresh_interval": 0, "ai": { "bridge": { - "anthropic_base_url": "string", + "anthropic": { + "base_url": "string", + "key": "string" + }, "daemons": 0, - "openai_base_url": "string" + "openai": { + "base_url": "string", + "key": "string" + } } }, "allow_workspace_renames": true, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index cebec57203450..22fffd456b4dd 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -335,32 +335,76 @@ | `groups` | array of [codersdk.Group](#codersdkgroup) | false | | | | `users` | array of [codersdk.ReducedUser](#codersdkreduceduser) | false | | | +## codersdk.AIBridgeAnthropicConfig + +```json +{ + "base_url": "string", + "key": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------|--------|----------|--------------|-------------| +| `base_url` | string | false | | | +| `key` | string | false | | | + ## codersdk.AIBridgeConfig ```json { - "anthropic_base_url": "string", + "anthropic": { + "base_url": "string", + "key": "string" + }, "daemons": 0, - "openai_base_url": "string" + "openai": { + "base_url": "string", + "key": "string" + } } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|----------------------|---------|----------|--------------|-------------| -| `anthropic_base_url` | string | false | | | -| `daemons` | integer | false | | | -| `openai_base_url` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|-------------|----------------------------------------------------------------------|----------|--------------|-------------| +| `anthropic` | [codersdk.AIBridgeAnthropicConfig](#codersdkaibridgeanthropicconfig) | false | | | +| `daemons` | integer | false | | | +| `openai` | [codersdk.AIBridgeOpenAIConfig](#codersdkaibridgeopenaiconfig) | false | | | + +## codersdk.AIBridgeOpenAIConfig + +```json +{ + "base_url": "string", + "key": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------|--------|----------|--------------|-------------| +| `base_url` | string | false | | | +| `key` | string | false | | | ## codersdk.AIConfig ```json { "bridge": { - "anthropic_base_url": "string", + "anthropic": { + "base_url": "string", + "key": "string" + }, "daemons": 0, - "openai_base_url": "string" + "openai": { + "base_url": "string", + "key": "string" + } } } ``` @@ -2190,9 +2234,15 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "agent_stat_refresh_interval": 0, "ai": { "bridge": { - "anthropic_base_url": "string", + "anthropic": { + "base_url": "string", + "key": "string" + }, "daemons": 0, - "openai_base_url": "string" + "openai": { + "base_url": "string", + "key": "string" + } } }, "allow_workspace_renames": true, @@ -2684,9 +2734,15 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "agent_stat_refresh_interval": 0, "ai": { "bridge": { - "anthropic_base_url": "string", + "anthropic": { + "base_url": "string", + "key": "string" + }, "daemons": 0, - "openai_base_url": "string" + "openai": { + "base_url": "string", + "key": "string" + } } }, "allow_workspace_renames": true, From f5b4f493006dcf18754ab0c76fa37b9580e1226c Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 8 Aug 2025 18:28:30 +0200 Subject: [PATCH 55/61] cleaner abstraction Signed-off-by: Danny Kopping --- aibridged/bridge.go | 148 +----------- aibridged/bridge_integration_test.go | 4 +- aibridged/proto/aibridged.pb.go | 213 +++++++++--------- aibridged/proto/aibridged.proto | 11 +- aibridged/provider.go | 11 +- aibridged/provider_anthropic.go | 46 ++++ aibridged/provider_anthropic_messages.go | 41 ---- aibridged/provider_openai.go | 67 ++++++ aibridged/provider_openai_chat.go | 60 ----- aibridged/session.go | 59 ++++- aibridged/session_anthropic_messages_base.go | 29 +-- .../session_anthropic_messages_blocking.go | 20 +- .../session_anthropic_messages_streaming.go | 26 +-- aibridged/session_openai_chat_base.go | 29 +-- aibridged/session_openai_chat_blocking.go | 38 +--- aibridged/session_openai_chat_streaming.go | 20 +- aibridged/tool_manager.go | 4 +- aibridged/tracker.go | 12 +- coderd/aibridgedserver/aibridgedserver.go | 10 +- coderd/database/dbauthz/dbauthz.go | 2 +- coderd/database/dbmetrics/querymetrics.go | 6 +- coderd/database/dbmock/dbmock.go | 7 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 11 +- coderd/database/queries/aibridge.sql | 5 +- 25 files changed, 379 insertions(+), 502 deletions(-) create mode 100644 aibridged/provider_anthropic.go delete mode 100644 aibridged/provider_anthropic_messages.go create mode 100644 aibridged/provider_openai.go delete mode 100644 aibridged/provider_openai_chat.go diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 73707361bba85..9a0773cdd08ac 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -1,9 +1,6 @@ package aibridged import ( - "context" - "fmt" - "io" "net/http" "time" @@ -11,8 +8,6 @@ import ( "cdr.dev/slog" - "github.com/google/uuid" - "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/codersdk" ) @@ -39,20 +34,18 @@ type Bridge struct { tools map[string]*MCPTool } -func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, error), tools map[string][]*MCPTool) (*Bridge, error) { - var bridge Bridge - - openAIChatProvider := NewOpenAIChatProvider(cfg.OpenAI.BaseURL.String(), cfg.OpenAI.Key.String()) - anthropicMessagesProvider := NewAnthropicMessagesProvider(cfg.Anthropic.BaseURL.String(), cfg.Anthropic.Key.String()) - +func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, error), tools ToolRegistry) (*Bridge, error) { drpcClient, err := clientFn() if err != nil { return nil, xerrors.Errorf("could not acquire coderd client for tracking: %w", err) } + openAIProvider := NewOpenAIProvider(cfg.OpenAI.BaseURL.String(), cfg.OpenAI.Key.String()) + anthropicMessagesProvider := NewAnthropicMessagesProvider(cfg.Anthropic.BaseURL.String(), cfg.Anthropic.Key.String()) + mux := &http.ServeMux{} - mux.HandleFunc("/v1/chat/completions", handleOpenAIChat(openAIChatProvider, drpcClient, tools, logger.Named("openai"))) - mux.HandleFunc("/v1/messages", handleAnthropicMessages(anthropicMessagesProvider, drpcClient, tools, logger.Named("anthropic"))) + mux.HandleFunc("/v1/chat/completions", NewSessionProcessor(openAIProvider, logger, drpcClient, tools)) + mux.HandleFunc("/v1/messages", NewSessionProcessor(anthropicMessagesProvider, logger, drpcClient, tools)) srv := &http.Server{ Handler: mux, @@ -64,6 +57,7 @@ func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() ReadHeaderTimeout: 10 * time.Second, } + var bridge Bridge bridge.cfg = cfg bridge.httpSrv = srv bridge.clientFn = clientFn @@ -82,131 +76,3 @@ func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() func (b *Bridge) Handler() http.Handler { return b.httpSrv.Handler } - -func handleOpenAIChat(provider *OpenAIChatProvider, drpcClient proto.DRPCAIBridgeDaemonClient, tools map[string][]*MCPTool, logger slog.Logger) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // Read and parse request. - body, err := io.ReadAll(r.Body) - if err != nil { - if isConnectionError(err) { - logger.Debug(r.Context(), "client disconnected during request body read", slog.Error(err)) - return // Don't send error response if client already disconnected - } - logger.Error(r.Context(), "failed to read body", slog.Error(err)) - http.Error(w, "failed to read body", http.StatusInternalServerError) - return - } - req, err := provider.ParseRequest(body) - if err != nil { - logger.Error(r.Context(), "failed to parse request", slog.Error(err)) - http.Error(w, "failed to parse request", http.StatusBadRequest) - return - } - - // Create a new session. - var sess Session - if req.Stream { - sess = provider.NewStreamingSession(req) - } else { - sess = provider.NewBlockingSession(req) - } - - userID, ok := r.Context().Value(ContextKeyBridgeUserID{}).(uuid.UUID) - if !ok { - logger.Error(r.Context(), "missing initiator ID in context") - http.Error(w, "unable to retrieve initiator", http.StatusInternalServerError) - return - } - - resp, err := drpcClient.StartSession(r.Context(), &proto.StartSessionRequest{ - InitiatorId: userID.String(), - Provider: "openai", - Model: req.Model, - }) - if err != nil { - logger.Error(r.Context(), "failed to start session", slog.Error(err)) - http.Error(w, "failed to start session", http.StatusInternalServerError) - return - } - - sessID := resp.GetSessionId() - - sess.Init(sessID, logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) - logger.Debug(context.Background(), "starting openai session", slog.F("session_id", sessID)) - - defer func() { - if err := sess.Close(); err != nil { - logger.Warn(context.Background(), "failed to close session", slog.Error(err), slog.F("session_id", sessID), slog.F("kind", fmt.Sprintf("%T", sess))) - } - }() - - // Process the request. - if err := sess.ProcessRequest(w, r); err != nil { - logger.Error(r.Context(), "session execution failed", slog.Error(err)) - } - } -} - -func handleAnthropicMessages(provider *AnthropicMessagesProvider, drpcClient proto.DRPCAIBridgeDaemonClient, tools map[string][]*MCPTool, logger slog.Logger) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // Read and parse request. - body, err := io.ReadAll(r.Body) - if err != nil { - if isConnectionError(err) { - logger.Debug(r.Context(), "client disconnected during request body read", slog.Error(err)) - return // Don't send error response if client already disconnected - } - logger.Error(r.Context(), "failed to read body", slog.Error(err)) - http.Error(w, "failed to read body", http.StatusInternalServerError) - return - } - req, err := provider.ParseRequest(body) - if err != nil { - logger.Error(r.Context(), "failed to parse request", slog.Error(err)) - http.Error(w, "failed to parse request", http.StatusBadRequest) - return - } - - // Create a new session. - var sess Session - if req.UseStreaming() { - sess = provider.NewStreamingSession(req) - } else { - sess = provider.NewBlockingSession(req) - } - - userID, ok := r.Context().Value(ContextKeyBridgeUserID{}).(uuid.UUID) - if !ok { - logger.Error(r.Context(), "missing initiator ID in context") - http.Error(w, "unable to retrieve initiator", http.StatusInternalServerError) - return - } - - resp, err := drpcClient.StartSession(r.Context(), &proto.StartSessionRequest{ - InitiatorId: userID.String(), - Provider: "anthropic", - Model: string(req.Model), - }) - if err != nil { - logger.Error(r.Context(), "failed to start session", slog.Error(err)) - http.Error(w, "failed to start session", http.StatusInternalServerError) - return - } - - sessID := resp.GetSessionId() - - sess.Init(sessID, logger, provider.baseURL, provider.key, NewDRPCTracker(drpcClient), NewInjectedToolManager(tools)) - logger.Debug(context.Background(), "starting anthropic messages session", slog.F("session_id", sessID)) - - defer func() { - if err := sess.Close(); err != nil { - logger.Warn(context.Background(), "failed to close session", slog.Error(err), slog.F("session_id", sessID), slog.F("kind", fmt.Sprintf("%T", sess))) - } - }() - - // Process the request. - if err := sess.ProcessRequest(w, r); err != nil { - logger.Error(r.Context(), "session execution failed", slog.Error(err)) - } - } -} diff --git a/aibridged/bridge_integration_test.go b/aibridged/bridge_integration_test.go index 3fbeceea152c6..7fac3a81bc82f 100644 --- a/aibridged/bridge_integration_test.go +++ b/aibridged/bridge_integration_test.go @@ -866,9 +866,7 @@ func (f *fakeBridgeDaemonClient) StartSession(ctx context.Context, in *proto.Sta defer f.mu.Unlock() f.sessions = append(f.sessions, in) - return &proto.StartSessionResponse{ - SessionId: uuid.NewString(), - }, nil + return &proto.StartSessionResponse{}, nil } func (f *fakeBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go index 4760f31fafbd5..08c43c98a5bd5 100644 --- a/aibridged/proto/aibridged.pb.go +++ b/aibridged/proto/aibridged.pb.go @@ -26,9 +26,10 @@ type StartSessionRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - InitiatorId string `protobuf:"bytes,1,opt,name=initiator_id,json=initiatorId,proto3" json:"initiator_id,omitempty"` - Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` - Model string `protobuf:"bytes,3,opt,name=model,proto3" json:"model,omitempty"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + InitiatorId string `protobuf:"bytes,2,opt,name=initiator_id,json=initiatorId,proto3" json:"initiator_id,omitempty"` + Provider string `protobuf:"bytes,3,opt,name=provider,proto3" json:"provider,omitempty"` + Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` } func (x *StartSessionRequest) Reset() { @@ -63,6 +64,13 @@ func (*StartSessionRequest) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} } +func (x *StartSessionRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + func (x *StartSessionRequest) GetInitiatorId() string { if x != nil { return x.InitiatorId @@ -88,8 +96,6 @@ type StartSessionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - - SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` } func (x *StartSessionResponse) Reset() { @@ -124,13 +130,6 @@ func (*StartSessionResponse) Descriptor() ([]byte, []int) { return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} } -func (x *StartSessionResponse) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - type TrackTokenUsageRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -489,104 +488,104 @@ var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x6f, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, - 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6a, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, - 0x0a, 0x0c, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x89, 0x01, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x72, + 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x21, + 0x0a, 0x0c, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x49, - 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, + 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, - 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, - 0x64, 0x65, 0x6c, 0x22, 0x35, 0x0a, 0x14, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xb6, 0x02, 0x0a, 0x16, 0x54, - 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, - 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x23, - 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x73, 0x12, 0x4b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, - 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x86, - 0x02, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, - 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, - 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x4b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x69, 0x62, 0x72, - 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, - 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0xb2, 0x02, 0x0a, 0x15, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, - 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, - 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, - 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, - 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x4a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x69, 0x62, - 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, - 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x32, 0xec, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4f, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, - 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, - 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, - 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, - 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, - 0x70, 0x74, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, - 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, - 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, 0x61, - 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x69, + 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, + 0x64, 0x65, 0x6c, 0x22, 0x16, 0x0a, 0x14, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb6, 0x02, 0x0a, 0x16, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, + 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, + 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x4b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, + 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x86, 0x02, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, + 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, + 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x4b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x69, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, + 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0xb2, 0x02, 0x0a, 0x15, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, + 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, + 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, + 0x67, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x4a, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, - 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, - 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, - 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x32, 0xec, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4f, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, + 0x64, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, + 0x64, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, + 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, + 0x6d, 0x70, 0x74, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, + 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, + 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, + 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, + 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, + 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, + 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, + 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto index e5c43d33378cf..27351b5cd2672 100644 --- a/aibridged/proto/aibridged.proto +++ b/aibridged/proto/aibridged.proto @@ -6,14 +6,13 @@ package aibridged; import "google/protobuf/any.proto"; message StartSessionRequest { - string initiator_id = 1; - string provider = 2; - string model = 3; + string session_id = 1; + string initiator_id = 2; + string provider = 3; + string model = 4; } -message StartSessionResponse { - string session_id = 1; -} +message StartSessionResponse {} message TrackTokenUsageRequest { string session_id = 1; diff --git a/aibridged/provider.go b/aibridged/provider.go index 1d45520ac7d6f..e8d5dc412e3e3 100644 --- a/aibridged/provider.go +++ b/aibridged/provider.go @@ -1,7 +1,10 @@ package aibridged -type Provider[Req any] interface { - ParseRequest(payload []byte) (*Req, error) - NewStreamingSession(*Req) Session - NewBlockingSession(*Req) Session +import ( + "net/http" +) + +type Provider interface { + CreateSession(w http.ResponseWriter, r *http.Request, tools ToolRegistry) (Session, error) + Identifier() string } diff --git a/aibridged/provider_anthropic.go b/aibridged/provider_anthropic.go new file mode 100644 index 0000000000000..403d8e3d00779 --- /dev/null +++ b/aibridged/provider_anthropic.go @@ -0,0 +1,46 @@ +package aibridged + +import ( + "encoding/json" + "io" + "net/http" + + "golang.org/x/xerrors" +) + +var _ Provider = &AnthropicMessagesProvider{} + +// AnthropicMessagesProvider allows for interactions with the Anthropic Messages API. +// See https://docs.anthropic.com/en/api/messages +type AnthropicMessagesProvider struct { + baseURL, key string +} + +func NewAnthropicMessagesProvider(baseURL, key string) *AnthropicMessagesProvider { + return &AnthropicMessagesProvider{ + baseURL: baseURL, + key: key, + } +} + +func (p *AnthropicMessagesProvider) CreateSession(w http.ResponseWriter, r *http.Request, tools ToolRegistry) (Session, error) { + payload, err := io.ReadAll(r.Body) + if err != nil { + return nil, xerrors.Errorf("read body: %w", err) + } + + var req BetaMessageNewParamsWrapper + if err := json.Unmarshal(payload, &req); err != nil { + return nil, xerrors.Errorf("failed to unmarshal request: %w", err) + } + + if req.Stream { + return NewAnthropicMessagesStreamingSession(&req, p.baseURL, p.key), nil + } + + return NewAnthropicMessagesBlockingSession(&req, p.baseURL, p.key), nil +} + +func (p *AnthropicMessagesProvider) Identifier() string { + return "anthropic" +} diff --git a/aibridged/provider_anthropic_messages.go b/aibridged/provider_anthropic_messages.go deleted file mode 100644 index 3031fe269fdbf..0000000000000 --- a/aibridged/provider_anthropic_messages.go +++ /dev/null @@ -1,41 +0,0 @@ -package aibridged - -import ( - "encoding/json" - - "golang.org/x/xerrors" -) - -var _ Provider[BetaMessageNewParamsWrapper] = &AnthropicMessagesProvider{} - -// AnthropicMessagesProvider allows for interactions with the Anthropic Messages API. -// See https://docs.anthropic.com/en/api/messages -type AnthropicMessagesProvider struct { - baseURL, key string -} - -func NewAnthropicMessagesProvider(baseURL, key string) *AnthropicMessagesProvider { - return &AnthropicMessagesProvider{ - baseURL: baseURL, - key: key, - } -} - -func (*AnthropicMessagesProvider) ParseRequest(payload []byte) (*BetaMessageNewParamsWrapper, error) { - var in BetaMessageNewParamsWrapper - if err := json.Unmarshal(payload, &in); err != nil { - return nil, xerrors.Errorf("failed to unmarshal request: %w", err) - } - - return &in, nil -} - -// NewStreamingSession creates a new session which handles streaming message completions. -func (*AnthropicMessagesProvider) NewStreamingSession(req *BetaMessageNewParamsWrapper) Session { - return NewAnthropicMessagesStreamingSession(req) -} - -// NewBlockingSession creates a new session which handles non-streaming message completions. -func (*AnthropicMessagesProvider) NewBlockingSession(req *BetaMessageNewParamsWrapper) Session { - return NewAnthropicMessagesBlockingSession(req) -} diff --git a/aibridged/provider_openai.go b/aibridged/provider_openai.go new file mode 100644 index 0000000000000..c27bf759d3b1c --- /dev/null +++ b/aibridged/provider_openai.go @@ -0,0 +1,67 @@ +package aibridged + +import ( + "encoding/json" + "io" + "net/http" + "os" + + "github.com/openai/openai-go" + "github.com/openai/openai-go/option" + "golang.org/x/xerrors" +) + +var _ Provider = &OpenAIProvider{} + +// OpenAIProvider allows for interactions with the Chat Completions API. +// See https://platform.openai.com/docs/api-reference/chat. +type OpenAIProvider struct { + baseURL, key string +} + +func NewOpenAIProvider(baseURL, key string) *OpenAIProvider { + return &OpenAIProvider{ + baseURL: baseURL, + key: key, + } +} + +func (p *OpenAIProvider) Identifier() string { + return "openai" +} + +func (p *OpenAIProvider) CreateSession(w http.ResponseWriter, r *http.Request, tools ToolRegistry) (Session, error) { + payload, err := io.ReadAll(r.Body) + if err != nil { + return nil, xerrors.Errorf("read body: %w", err) + } + + switch r.URL.Path { + case "/v1/chat/completions": + var req ChatCompletionNewParamsWrapper + if err := json.Unmarshal(payload, &req); err != nil { + return nil, xerrors.Errorf("unmarshal request body: %w", err) + } + + if req.Stream { + return NewOpenAIStreamingChatSession(&req, p.baseURL, p.key), nil + } else { + return NewOpenAIBlockingChatSession(&req, p.baseURL, p.key), nil + } + } + + return nil, UnknownRoute +} + +func newOpenAIClient(baseURL, key string) openai.Client { + var opts []option.RequestOption + if key == "" { + key = os.Getenv("OPENAI_API_KEY") + } + opts = append(opts, option.WithAPIKey(key)) + if baseURL != "" { + opts = append(opts, option.WithBaseURL(baseURL)) + } + + return openai.NewClient(opts...) +} diff --git a/aibridged/provider_openai_chat.go b/aibridged/provider_openai_chat.go deleted file mode 100644 index 9d26ffeced583..0000000000000 --- a/aibridged/provider_openai_chat.go +++ /dev/null @@ -1,60 +0,0 @@ -package aibridged - -import ( - "encoding/json" - "os" - - "github.com/openai/openai-go" - "github.com/openai/openai-go/option" - "golang.org/x/xerrors" -) - -var _ Provider[ChatCompletionNewParamsWrapper] = &OpenAIChatProvider{} - -// OpenAIChatProvider allows for interactions with the Chat Completions API. -// See https://platform.openai.com/docs/api-reference/chat. -type OpenAIChatProvider struct { - baseURL, key string -} - -func NewOpenAIChatProvider(baseURL, key string) *OpenAIChatProvider { - return &OpenAIChatProvider{ - baseURL: baseURL, - key: key, - } -} - -func (*OpenAIChatProvider) ParseRequest(payload []byte) (*ChatCompletionNewParamsWrapper, error) { - var in ChatCompletionNewParamsWrapper - if err := json.Unmarshal(payload, &in); err != nil { - return nil, xerrors.Errorf("failed to unmarshal request: %w", err) - } - - return &in, nil -} - -// NewStreamingSession creates a new session which handles streaming chat completions. -// See https://platform.openai.com/docs/api-reference/chat-streaming -func (*OpenAIChatProvider) NewStreamingSession(req *ChatCompletionNewParamsWrapper) Session { - return NewOpenAIStreamingChatSession(req) -} - -// NewBlockingSession creates a new session which handles non-streaming chat completions. -// See https://platform.openai.com/docs/api-reference/chat -func (*OpenAIChatProvider) NewBlockingSession(req *ChatCompletionNewParamsWrapper) Session { - return NewOpenAIBlockingChatSession(req) - -} - -func newOpenAIClient(baseURL, key string) openai.Client { - var opts []option.RequestOption - if key == "" { - key = os.Getenv("OPENAI_API_KEY") - } - opts = append(opts, option.WithAPIKey(key)) - if baseURL != "" { - opts = append(opts, option.WithBaseURL(baseURL)) - } - - return openai.NewClient(opts...) -} diff --git a/aibridged/session.go b/aibridged/session.go index 40634ccf72523..00656ec344f78 100644 --- a/aibridged/session.go +++ b/aibridged/session.go @@ -1,20 +1,63 @@ package aibridged import ( + "context" + "fmt" "net/http" + "github.com/google/uuid" + "golang.org/x/xerrors" + "cdr.dev/slog" + "github.com/coder/coder/v2/aibridged/proto" ) -type Model struct { - Provider, ModelName string -} - // Session describes a (potentially) stateful interaction with an AI provider. type Session interface { - Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) - LastUserPrompt() (*string, error) - Model() Model + Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string + Model() string ProcessRequest(w http.ResponseWriter, r *http.Request) error - Close() error +} + +var ( + UnknownRoute = xerrors.New("unknown route") +) + +func NewSessionProcessor(p Provider, logger slog.Logger, client proto.DRPCAIBridgeDaemonClient, tools ToolRegistry) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + sess, err := p.CreateSession(w, r, tools) + if err != nil { + logger.Error(r.Context(), "failed to create session", slog.Error(err), slog.F("path", r.URL.Path)) + http.Error(w, fmt.Sprintf("failed to create %q session", r.URL.Path), http.StatusInternalServerError) + return + } + + sessID := sess.Init(logger, NewDRPCTracker(client), NewInjectedToolManager(tools)) + logger = logger.With(slog.F("route", r.URL.Path), slog.F("provider", p.Identifier()), slog.F("session_id", sessID)) + + userID, ok := r.Context().Value(ContextKeyBridgeUserID{}).(uuid.UUID) + if !ok { + logger.Error(r.Context(), "missing initiator ID in context") + http.Error(w, "unable to retrieve initiator", http.StatusInternalServerError) + return + } + + _, err = client.StartSession(r.Context(), &proto.StartSessionRequest{ + SessionId: sessID, + InitiatorId: userID.String(), + Provider: p.Identifier(), + Model: sess.Model(), + }) + if err != nil { + logger.Error(r.Context(), "failed to start session", slog.Error(err)) + http.Error(w, "failed to start session", http.StatusInternalServerError) + return + } + + logger.Debug(context.Background(), "started session") + + if err := sess.ProcessRequest(w, r); err != nil { + logger.Error(r.Context(), "session execution failed", slog.Error(err)) + } + } } diff --git a/aibridged/session_anthropic_messages_base.go b/aibridged/session_anthropic_messages_base.go index ed750cf364ca3..3b7edc6191e81 100644 --- a/aibridged/session_anthropic_messages_base.go +++ b/aibridged/session_anthropic_messages_base.go @@ -4,7 +4,7 @@ import ( "strings" "github.com/anthropics/anthropic-sdk-go" - "golang.org/x/xerrors" + "github.com/google/uuid" "cdr.dev/slog" ) @@ -21,38 +21,23 @@ type AnthropicMessagesSessionBase struct { toolMgr ToolManager } -func (s *AnthropicMessagesSessionBase) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { - s.id = id +func (s *AnthropicMessagesSessionBase) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { + s.id = uuid.NewString() s.logger = logger.With(slog.F("session_id", s.id)) - s.baseURL = baseURL - s.key = key - s.tracker = tracker s.toolMgr = toolMgr -} - -func (s *AnthropicMessagesSessionBase) LastUserPrompt() (*string, error) { - if s.req == nil { - return nil, xerrors.New("nil request") - } - return s.req.LastUserPrompt() + return s.id } -func (s *AnthropicMessagesSessionBase) Model() Model { - var model string +func (s *AnthropicMessagesSessionBase) Model() string { if s.req == nil { - model = "?" - } else { - model = string(s.req.Model) + return "?" } - return Model{ - Provider: "anthropic", - ModelName: model, - } + return string(s.req.Model) } func (s *AnthropicMessagesSessionBase) injectTools() { diff --git a/aibridged/session_anthropic_messages_blocking.go b/aibridged/session_anthropic_messages_blocking.go index 626cac186ac7e..669b96d50bd41 100644 --- a/aibridged/session_anthropic_messages_blocking.go +++ b/aibridged/session_anthropic_messages_blocking.go @@ -21,14 +21,16 @@ type AnthropicMessagesBlockingSession struct { AnthropicMessagesSessionBase } -func NewAnthropicMessagesBlockingSession(req *BetaMessageNewParamsWrapper) *AnthropicMessagesBlockingSession { +func NewAnthropicMessagesBlockingSession(req *BetaMessageNewParamsWrapper, baseURL, key string) *AnthropicMessagesBlockingSession { return &AnthropicMessagesBlockingSession{AnthropicMessagesSessionBase: AnthropicMessagesSessionBase{ - req: req, + req: req, + baseURL: baseURL, + key: key, }} } -func (s *AnthropicMessagesBlockingSession) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { - s.AnthropicMessagesSessionBase.Init(id, logger.Named("blocking"), baseURL, key, tracker, toolMgr) +func (s *AnthropicMessagesBlockingSession) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { + return s.AnthropicMessagesSessionBase.Init(logger.Named("blocking"), tracker, toolMgr) } func (s *AnthropicMessagesBlockingSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { @@ -46,7 +48,7 @@ func (s *AnthropicMessagesBlockingSession) ProcessRequest(w http.ResponseWriter, ) // Track user prompt if not a small/fast model if !s.isSmallFastModel() { - prompt, err = s.LastUserPrompt() + prompt, err = s.req.LastUserPrompt() if err != nil { s.logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) } @@ -85,12 +87,12 @@ func (s *AnthropicMessagesBlockingSession) ProcessRequest(w http.ResponseWriter, } if prompt != nil { - if err := s.tracker.TrackPromptUsage(ctx, s.id, resp.ID, s.Model(), *prompt, nil); err != nil { + if err := s.tracker.TrackPromptUsage(ctx, s.id, resp.ID, *prompt, nil); err != nil { s.logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) } } - if err := s.tracker.TrackTokensUsage(ctx, s.id, resp.ID, s.Model(), resp.Usage.InputTokens, resp.Usage.OutputTokens, Metadata{ + if err := s.tracker.TrackTokensUsage(ctx, s.id, resp.ID, resp.Usage.InputTokens, resp.Usage.OutputTokens, Metadata{ "web_search_requests": resp.Usage.ServerToolUse.WebSearchRequests, "cache_creation_input": resp.Usage.CacheCreationInputTokens, "cache_read_input": resp.Usage.CacheReadInputTokens, @@ -114,7 +116,7 @@ func (s *AnthropicMessagesBlockingSession) ProcessRequest(w http.ResponseWriter, } // If tool is not injected, track it since the client will be handling it. - if err := s.tracker.TrackToolUsage(ctx, s.id, resp.ID, s.Model(), toolUse.Name, toolUse.Input, false, nil); err != nil { + if err := s.tracker.TrackToolUsage(ctx, s.id, resp.ID, toolUse.Name, toolUse.Input, false, nil); err != nil { logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) } } @@ -163,7 +165,7 @@ func (s *AnthropicMessagesBlockingSession) ProcessRequest(w http.ResponseWriter, if _, tool, err := DecodeToolID(toolName); err == nil { toolName = tool } - if err := s.tracker.TrackToolUsage(ctx, s.id, resp.ID, s.Model(), toolName, args, true, nil); err != nil { + if err := s.tracker.TrackToolUsage(ctx, s.id, resp.ID, toolName, args, true, nil); err != nil { logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) } diff --git a/aibridged/session_anthropic_messages_streaming.go b/aibridged/session_anthropic_messages_streaming.go index 1c134c0c83a8f..dff5d45b7ba31 100644 --- a/aibridged/session_anthropic_messages_streaming.go +++ b/aibridged/session_anthropic_messages_streaming.go @@ -23,14 +23,16 @@ type AnthropicMessagesStreamingSession struct { AnthropicMessagesSessionBase } -func NewAnthropicMessagesStreamingSession(req *BetaMessageNewParamsWrapper) *AnthropicMessagesStreamingSession { +func NewAnthropicMessagesStreamingSession(req *BetaMessageNewParamsWrapper, baseURL, key string) *AnthropicMessagesStreamingSession { return &AnthropicMessagesStreamingSession{AnthropicMessagesSessionBase: AnthropicMessagesSessionBase{ - req: req, + req: req, + baseURL: baseURL, + key: key, }} } -func (s *AnthropicMessagesStreamingSession) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { - s.AnthropicMessagesSessionBase.Init(id, logger.Named("streaming"), baseURL, key, tracker, toolMgr) +func (s *AnthropicMessagesStreamingSession) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { + return s.AnthropicMessagesSessionBase.Init(logger.Named("streaming"), tracker, toolMgr) } func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { @@ -47,11 +49,11 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter // Track user prompt if not a small/fast model if !s.isSmallFastModel() { - prompt, err := s.LastUserPrompt() + prompt, err := s.req.LastUserPrompt() if err != nil { s.logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) } else if prompt != nil { - if err := s.tracker.TrackPromptUsage(ctx, s.id, "", s.Model(), *prompt, nil); err != nil { + if err := s.tracker.TrackPromptUsage(ctx, s.id, "", *prompt, nil); err != nil { s.logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) } } @@ -141,7 +143,7 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter "cache_ephemeral_1h_input": start.Message.Usage.CacheCreation.Ephemeral1hInputTokens, "cache_ephemeral_5m_input": start.Message.Usage.CacheCreation.Ephemeral5mInputTokens, } - if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, s.Model(), start.Message.Usage.InputTokens, start.Message.Usage.OutputTokens, metadata); err != nil { + if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, start.Message.Usage.InputTokens, start.Message.Usage.OutputTokens, metadata); err != nil { logger.Warn(ctx, "failed to track token usage", slog.Error(err)) } @@ -162,7 +164,7 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter "cache_ephemeral_1h_input": 0, "cache_ephemeral_5m_input": 0, } - if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, s.Model(), delta.Usage.InputTokens, delta.Usage.OutputTokens, metadata); err != nil { + if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, delta.Usage.InputTokens, delta.Usage.OutputTokens, metadata); err != nil { logger.Warn(ctx, "failed to track token usage", slog.Error(err)) } @@ -224,7 +226,7 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter if _, decodedTool, err := DecodeToolID(toolName); err == nil { toolName = decodedTool } - if err := s.tracker.TrackToolUsage(streamCtx, s.id, message.ID, s.Model(), toolName, input, true, nil); err != nil { + if err := s.tracker.TrackToolUsage(streamCtx, s.id, message.ID, toolName, input, true, nil); err != nil { logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) } @@ -325,7 +327,7 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter continue } - if err := s.tracker.TrackToolUsage(streamCtx, s.id, message.ID, s.Model(), variant.Name, variant.Input, false, nil); err != nil { + if err := s.tracker.TrackToolUsage(streamCtx, s.id, message.ID, variant.Name, variant.Input, false, nil); err != nil { logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) } } @@ -380,7 +382,3 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter return nil } - -func (s *AnthropicMessagesStreamingSession) Close() error { - return nil -} diff --git a/aibridged/session_openai_chat_base.go b/aibridged/session_openai_chat_base.go index 7f333015d01c7..ab6edb7c6f8c1 100644 --- a/aibridged/session_openai_chat_base.go +++ b/aibridged/session_openai_chat_base.go @@ -1,8 +1,8 @@ package aibridged import ( + "github.com/google/uuid" "github.com/openai/openai-go" - "golang.org/x/xerrors" "cdr.dev/slog" ) @@ -19,38 +19,23 @@ type OpenAIChatSessionBase struct { toolMgr ToolManager } -func (s *OpenAIChatSessionBase) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { - s.id = id +func (s *OpenAIChatSessionBase) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { + s.id = uuid.NewString() s.logger = logger.With(slog.F("session_id", s.id)) - s.baseURL = baseURL - s.key = key - s.tracker = tracker s.toolMgr = toolMgr -} - -func (s *OpenAIChatSessionBase) LastUserPrompt() (*string, error) { - if s.req == nil { - return nil, xerrors.New("nil request") - } - return s.req.LastUserPrompt() + return s.id } -func (s *OpenAIChatSessionBase) Model() Model { - var model string +func (s *OpenAIChatSessionBase) Model() string { if s.req == nil { - model = "?" - } else { - model = s.req.Model + return "?" } - return Model{ - Provider: "openai", - ModelName: model, - } + return string(s.req.Model) } func (s *OpenAIChatSessionBase) newErrorResponse(err error) map[string]interface{} { diff --git a/aibridged/session_openai_chat_blocking.go b/aibridged/session_openai_chat_blocking.go index 0dc14e3392de5..efb4d3ac5ee83 100644 --- a/aibridged/session_openai_chat_blocking.go +++ b/aibridged/session_openai_chat_blocking.go @@ -19,14 +19,16 @@ type OpenAIBlockingChatSession struct { OpenAIChatSessionBase } -func NewOpenAIBlockingChatSession(req *ChatCompletionNewParamsWrapper) *OpenAIBlockingChatSession { +func NewOpenAIBlockingChatSession(req *ChatCompletionNewParamsWrapper, baseURL, key string) *OpenAIBlockingChatSession { return &OpenAIBlockingChatSession{OpenAIChatSessionBase: OpenAIChatSessionBase{ - req: req, + req: req, + baseURL: baseURL, + key: key, }} } -func (s *OpenAIBlockingChatSession) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { - s.OpenAIChatSessionBase.Init(id, logger.Named("streaming"), baseURL, key, tracker, toolMgr) +func (s *OpenAIBlockingChatSession) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { + return s.OpenAIChatSessionBase.Init(logger.Named("blocking"), tracker, toolMgr) } func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { @@ -46,7 +48,7 @@ func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *htt s.injectTools() - prompt, err := s.LastUserPrompt() + prompt, err := s.req.LastUserPrompt() if err != nil { logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) } @@ -58,7 +60,7 @@ func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *htt } if prompt != nil { - if err := s.tracker.TrackPromptUsage(ctx, s.id, completion.ID, s.Model(), *prompt, nil); err != nil { + if err := s.tracker.TrackPromptUsage(ctx, s.id, completion.ID, *prompt, nil); err != nil { logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) } } @@ -67,7 +69,7 @@ func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *htt cumulativeUsage = sumUsage(cumulativeUsage, completion.Usage) // Track token usage - if err := s.tracker.TrackTokensUsage(ctx, s.id, completion.ID, s.Model(), cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ + if err := s.tracker.TrackTokensUsage(ctx, s.id, completion.ID, cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, @@ -85,7 +87,7 @@ func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *htt if s.toolMgr.GetTool(toolCall.Function.Name) != nil { pendingToolCalls = append(pendingToolCalls, toolCall) } else { - if err := s.tracker.TrackToolUsage(ctx, s.id, completion.ID, s.Model(), toolCall.Function.Name, toolCall.Function.Arguments, false, nil); err != nil { + if err := s.tracker.TrackToolUsage(ctx, s.id, completion.ID, toolCall.Function.Name, toolCall.Function.Arguments, false, nil); err != nil { s.logger.Warn(ctx, "failed to track tool usage", slog.Error(err), slog.F("tool", toolCall.Function.Name)) } } @@ -113,7 +115,7 @@ func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *htt appendedPrevMsg = true } - if err := s.tracker.TrackToolUsage(ctx, s.id, completion.ID, s.Model(), tool.Name, tc.Function.Arguments, true, nil); err != nil { + if err := s.tracker.TrackToolUsage(ctx, s.id, completion.ID, tool.Name, tc.Function.Arguments, true, nil); err != nil { logger.Warn(ctx, "failed to track tool usage", slog.Error(err), slog.F("tool", tool.Name)) } @@ -195,21 +197,3 @@ func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *htt return nil } - -func (s *OpenAIBlockingChatSession) Model() Model { - var model string - if s.req == nil { - model = "?" - } else { - model = s.req.Model - } - - return Model{ - Provider: "openai", - ModelName: model, - } -} - -func (s *OpenAIBlockingChatSession) Close() error { - return nil // TODO: do we even need this? -} diff --git a/aibridged/session_openai_chat_streaming.go b/aibridged/session_openai_chat_streaming.go index 847d8988826c3..c47d6bf9d141d 100644 --- a/aibridged/session_openai_chat_streaming.go +++ b/aibridged/session_openai_chat_streaming.go @@ -21,14 +21,16 @@ type OpenAIStreamingChatSession struct { OpenAIChatSessionBase } -func NewOpenAIStreamingChatSession(req *ChatCompletionNewParamsWrapper) *OpenAIStreamingChatSession { +func NewOpenAIStreamingChatSession(req *ChatCompletionNewParamsWrapper, baseURL, key string) *OpenAIStreamingChatSession { return &OpenAIStreamingChatSession{OpenAIChatSessionBase: OpenAIChatSessionBase{ - req: req, + req: req, + baseURL: baseURL, + key: key, }} } -func (s *OpenAIStreamingChatSession) Init(id string, logger slog.Logger, baseURL, key string, tracker Tracker, toolMgr ToolManager) { - s.OpenAIChatSessionBase.Init(id, logger.Named("streaming"), baseURL, key, tracker, toolMgr) +func (s *OpenAIStreamingChatSession) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { + return s.OpenAIChatSessionBase.Init(logger.Named("streaming"), tracker, toolMgr) } func (s *OpenAIStreamingChatSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { @@ -74,7 +76,7 @@ func (s *OpenAIStreamingChatSession) ProcessRequest(w http.ResponseWriter, r *ht s.req.ParallelToolCalls = openai.Bool(false) } - prompt, err := s.LastUserPrompt() + prompt, err := s.req.LastUserPrompt() if err != nil { logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) } @@ -100,7 +102,7 @@ func (s *OpenAIStreamingChatSession) ProcessRequest(w http.ResponseWriter, r *ht // Don't relay this chunk back; we'll handle it transparently. shouldRelayChunk = false } else { - if err := s.tracker.TrackToolUsage(ctx, s.id, chunk.ID, s.Model(), toolCall.Name, toolCall.Arguments, false, nil); err != nil { + if err := s.tracker.TrackToolUsage(ctx, s.id, chunk.ID, toolCall.Name, toolCall.Arguments, false, nil); err != nil { logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) } } @@ -126,14 +128,14 @@ func (s *OpenAIStreamingChatSession) ProcessRequest(w http.ResponseWriter, r *ht } if prompt != nil { - if err := s.tracker.TrackPromptUsage(ctx, s.id, acc.ID, s.Model(), *prompt, nil); err != nil { + if err := s.tracker.TrackPromptUsage(ctx, s.id, acc.ID, *prompt, nil); err != nil { logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) } } // If the usage information is set, track it. // The API will send usage information when the response terminates, which will happen if a tool call is invoked. - if err := s.tracker.TrackTokensUsage(ctx, s.id, acc.ID, s.Model(), cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ + if err := s.tracker.TrackTokensUsage(ctx, s.id, acc.ID, cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, @@ -184,7 +186,7 @@ func (s *OpenAIStreamingChatSession) ProcessRequest(w http.ResponseWriter, r *ht toolName = tc.Name } - if err := s.tracker.TrackToolUsage(ctx, s.id, acc.ID, s.Model(), toolName, tc.Arguments, true, nil); err != nil { + if err := s.tracker.TrackToolUsage(ctx, s.id, acc.ID, toolName, tc.Arguments, true, nil); err != nil { logger.Warn(ctx, "failed to track tool usage", slog.Error(err), slog.F("tool", tool.Name)) } diff --git a/aibridged/tool_manager.go b/aibridged/tool_manager.go index 4a396f40a7685..415e8be3611b8 100644 --- a/aibridged/tool_manager.go +++ b/aibridged/tool_manager.go @@ -10,6 +10,8 @@ type ToolManager interface { ListTools() []*MCPTool } +type ToolRegistry map[string][]*MCPTool + var _ ToolManager = &InjectedToolManager{} // InjectedToolManager is responsible for all injected tools. @@ -28,7 +30,7 @@ type InjectedToolManager struct { // // -func NewInjectedToolManager(tools map[string][]*MCPTool) *InjectedToolManager { +func NewInjectedToolManager(tools ToolRegistry) *InjectedToolManager { tm := &InjectedToolManager{} for server, val := range tools { tm.AddTools(server, val) diff --git a/aibridged/tracker.go b/aibridged/tracker.go index 206cb902e0584..333d0ae3aa829 100644 --- a/aibridged/tracker.go +++ b/aibridged/tracker.go @@ -29,9 +29,9 @@ func (m Metadata) MarshalForProto() map[string]*anypb.Any { } type Tracker interface { - TrackTokensUsage(ctx context.Context, sessionID, msgID string, model Model, promptTokens, completionTokens int64, metadata Metadata) error - TrackPromptUsage(ctx context.Context, sessionID, msgID string, model Model, prompt string, metadata Metadata) error - TrackToolUsage(ctx context.Context, sessionID, msgID string, model Model, name string, args any, injected bool, metadata Metadata) error + TrackTokensUsage(ctx context.Context, sessionID, msgID string, promptTokens, completionTokens int64, metadata Metadata) error + TrackPromptUsage(ctx context.Context, sessionID, msgID, prompt string, metadata Metadata) error + TrackToolUsage(ctx context.Context, sessionID, msgID, name string, args any, injected bool, metadata Metadata) error } var _ Tracker = &DRPCTracker{} @@ -45,7 +45,7 @@ func NewDRPCTracker(client proto.DRPCAIBridgeDaemonClient) *DRPCTracker { return &DRPCTracker{client} } -func (d *DRPCTracker) TrackTokensUsage(ctx context.Context, sessionID, msgID string, model Model, promptTokens, completionTokens int64, metadata Metadata) error { +func (d *DRPCTracker) TrackTokensUsage(ctx context.Context, sessionID, msgID string, promptTokens, completionTokens int64, metadata Metadata) error { _, err := d.client.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ SessionId: sessionID, MsgId: msgID, @@ -56,7 +56,7 @@ func (d *DRPCTracker) TrackTokensUsage(ctx context.Context, sessionID, msgID str return err } -func (d *DRPCTracker) TrackPromptUsage(ctx context.Context, sessionID, msgID string, model Model, prompt string, metadata Metadata) error { +func (d *DRPCTracker) TrackPromptUsage(ctx context.Context, sessionID, msgID string, prompt string, metadata Metadata) error { _, err := d.client.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ SessionId: sessionID, MsgId: msgID, @@ -66,7 +66,7 @@ func (d *DRPCTracker) TrackPromptUsage(ctx context.Context, sessionID, msgID str return err } -func (d *DRPCTracker) TrackToolUsage(ctx context.Context, sessionID, msgID string, model Model, name string, args any, injected bool, metadata Metadata) error { +func (d *DRPCTracker) TrackToolUsage(ctx context.Context, sessionID, msgID, name string, args any, injected bool, metadata Metadata) error { var ( serialized []byte err error diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index 8a0a4bd3178d1..5e566fa70f2e0 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -36,13 +36,17 @@ func NewServer(lifecycleCtx context.Context, store database.Store, logger slog.L // StartSession implements proto.DRPCAIBridgeDaemonServer. func (s *Server) StartSession(ctx context.Context, in *proto.StartSessionRequest) (*proto.StartSessionResponse, error) { + sessID, err := uuid.Parse(in.GetSessionId()) + if err != nil { + return nil, xerrors.Errorf("invalid session ID %q: %w", in.GetSessionId(), err) + } initID, err := uuid.Parse(in.GetInitiatorId()) if err != nil { return nil, xerrors.Errorf("invalid initiator ID %q: %w", in.GetInitiatorId(), err) } - id, err := s.store.InsertAIBridgeSession(ctx, database.InsertAIBridgeSessionParams{ - ID: uuid.New(), + err = s.store.InsertAIBridgeSession(ctx, database.InsertAIBridgeSessionParams{ + ID: sessID, InitiatorID: initID, Provider: in.Provider, Model: in.Model, @@ -51,7 +55,7 @@ func (s *Server) StartSession(ctx context.Context, in *proto.StartSessionRequest return nil, xerrors.Errorf("start session: %w", err) } - return &proto.StartSessionResponse{SessionId: id.String()}, nil + return &proto.StartSessionResponse{}, nil } func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index a780c41a6c3e0..050e0d26dff2b 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3617,7 +3617,7 @@ func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now ti return q.db.GetWorkspacesEligibleForTransition(ctx, now) } -func (q *querier) InsertAIBridgeSession(ctx context.Context, arg database.InsertAIBridgeSessionParams) (uuid.UUID, error) { +func (q *querier) InsertAIBridgeSession(ctx context.Context, arg database.InsertAIBridgeSessionParams) error { // TODO: authz. return q.db.InsertAIBridgeSession(ctx, arg) } diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 8c35bb817bf26..233bb123a0050 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2098,11 +2098,11 @@ func (m queryMetricsStore) GetWorkspacesEligibleForTransition(ctx context.Contex return workspaces, err } -func (m queryMetricsStore) InsertAIBridgeSession(ctx context.Context, arg database.InsertAIBridgeSessionParams) (uuid.UUID, error) { +func (m queryMetricsStore) InsertAIBridgeSession(ctx context.Context, arg database.InsertAIBridgeSessionParams) error { start := time.Now() - r0, r1 := m.s.InsertAIBridgeSession(ctx, arg) + r0 := m.s.InsertAIBridgeSession(ctx, arg) m.queryLatencies.WithLabelValues("InsertAIBridgeSession").Observe(time.Since(start).Seconds()) - return r0, r1 + return r0 } func (m queryMetricsStore) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error { diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index ef9aaae2a40f1..8ac5078be62a0 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -4487,12 +4487,11 @@ func (mr *MockStoreMockRecorder) InTx(arg0, arg1 any) *gomock.Call { } // InsertAIBridgeSession mocks base method. -func (m *MockStore) InsertAIBridgeSession(ctx context.Context, arg database.InsertAIBridgeSessionParams) (uuid.UUID, error) { +func (m *MockStore) InsertAIBridgeSession(ctx context.Context, arg database.InsertAIBridgeSessionParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InsertAIBridgeSession", ctx, arg) - ret0, _ := ret[0].(uuid.UUID) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(error) + return ret0 } // InsertAIBridgeSession indicates an expected call of InsertAIBridgeSession. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 63bd037603110..9a4461b520bf1 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -473,7 +473,7 @@ type sqlcQuerier interface { GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error) - InsertAIBridgeSession(ctx context.Context, arg InsertAIBridgeSessionParams) (uuid.UUID, error) + InsertAIBridgeSession(ctx context.Context, arg InsertAIBridgeSessionParams) error InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) error InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) error InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2d9d363954f3b..60d12970898f3 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -107,10 +107,9 @@ func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBump return err } -const insertAIBridgeSession = `-- name: InsertAIBridgeSession :one +const insertAIBridgeSession = `-- name: InsertAIBridgeSession :exec INSERT INTO aibridge_sessions (id, initiator_id, provider, model) VALUES ($1::uuid, $2::uuid, $3, $4) -RETURNING $1::uuid ` type InsertAIBridgeSessionParams struct { @@ -120,16 +119,14 @@ type InsertAIBridgeSessionParams struct { Model string `db:"model" json:"model"` } -func (q *sqlQuerier) InsertAIBridgeSession(ctx context.Context, arg InsertAIBridgeSessionParams) (uuid.UUID, error) { - row := q.db.QueryRowContext(ctx, insertAIBridgeSession, +func (q *sqlQuerier) InsertAIBridgeSession(ctx context.Context, arg InsertAIBridgeSessionParams) error { + _, err := q.db.ExecContext(ctx, insertAIBridgeSession, arg.ID, arg.InitiatorID, arg.Provider, arg.Model, ) - var column_1 uuid.UUID - err := row.Scan(&column_1) - return column_1, err + return err } const insertAIBridgeTokenUsage = `-- name: InsertAIBridgeTokenUsage :exec diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index f8f30e88ae766..4089b5c9dec32 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -1,7 +1,6 @@ --- name: InsertAIBridgeSession :one +-- name: InsertAIBridgeSession :exec INSERT INTO aibridge_sessions (id, initiator_id, provider, model) -VALUES (@id::uuid, @initiator_id::uuid, @provider, @model) -RETURNING @id::uuid; +VALUES (@id::uuid, @initiator_id::uuid, @provider, @model); -- name: InsertAIBridgeTokenUsage :exec INSERT INTO aibridge_token_usages ( From f0691cba3ccdbb6fbca73f90fed01b4ddae438bd Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Fri, 8 Aug 2025 21:12:18 +0200 Subject: [PATCH 56/61] instantiate providers outside bridge Signed-off-by: Danny Kopping --- aibridged/anthropic.go | 1 + aibridged/bridge.go | 31 ++++++++---- aibridged/bridge_integration_test.go | 75 +++++++++++----------------- aibridged/middleware.go | 6 ++- aibridged/provider.go | 2 + aibridged/provider_anthropic.go | 2 +- aibridged/provider_openai.go | 2 +- aibridged/session.go | 4 +- aibridged/streaming.go | 4 +- coderd/aibridge.go | 7 ++- 10 files changed, 67 insertions(+), 67 deletions(-) diff --git a/aibridged/anthropic.go b/aibridged/anthropic.go index 99838c711ecf6..c2b52067d6579 100644 --- a/aibridged/anthropic.go +++ b/aibridged/anthropic.go @@ -134,6 +134,7 @@ func (b *MessageNewParamsWrapper) UnmarshalJSON(raw []byte) error { b.Stream = extractStreamFlag(raw) return nil } + func (b *MessageNewParamsWrapper) UseStreaming() bool { return b.Stream } diff --git a/aibridged/bridge.go b/aibridged/bridge.go index 9a0773cdd08ac..e5f14e5acecd4 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -9,9 +9,18 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/aibridged/proto" - "github.com/coder/coder/v2/codersdk" ) +const ( + ProviderOpenAI = "openai" + ProviderAnthropic = "anthropic" +) + +var providerRoutes = map[string][]string{ + ProviderOpenAI: {"/v1/chat/completions"}, + ProviderAnthropic: {"/v1/messages"}, +} + // Bridge is responsible for proxying requests to upstream AI providers. // // Characteristics: @@ -25,8 +34,6 @@ import ( // 6. multiple calls can be made to provider while holding client<->coderd conn open // 7. client<->coderd conn must ONLY be closed on client-side disconn or coderd<->provider non-recoverable error. type Bridge struct { - cfg codersdk.AIBridgeConfig - httpSrv *http.Server clientFn func() (proto.DRPCAIBridgeDaemonClient, error) logger slog.Logger @@ -34,18 +41,23 @@ type Bridge struct { tools map[string]*MCPTool } -func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, error), tools ToolRegistry) (*Bridge, error) { +func NewBridge(registry ProviderRegistry, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, error), tools ToolRegistry) (*Bridge, error) { drpcClient, err := clientFn() if err != nil { return nil, xerrors.Errorf("could not acquire coderd client for tracking: %w", err) } - openAIProvider := NewOpenAIProvider(cfg.OpenAI.BaseURL.String(), cfg.OpenAI.Key.String()) - anthropicMessagesProvider := NewAnthropicMessagesProvider(cfg.Anthropic.BaseURL.String(), cfg.Anthropic.Key.String()) - mux := &http.ServeMux{} - mux.HandleFunc("/v1/chat/completions", NewSessionProcessor(openAIProvider, logger, drpcClient, tools)) - mux.HandleFunc("/v1/messages", NewSessionProcessor(anthropicMessagesProvider, logger, drpcClient, tools)) + for ident, provider := range registry { + routes, ok := providerRoutes[ident] + if !ok { + // Unknown provider identifier; skip. + continue + } + for _, path := range routes { + mux.HandleFunc(path, NewSessionProcessor(provider, logger, drpcClient, tools)) + } + } srv := &http.Server{ Handler: mux, @@ -58,7 +70,6 @@ func NewBridge(cfg codersdk.AIBridgeConfig, logger slog.Logger, clientFn func() } var bridge Bridge - bridge.cfg = cfg bridge.httpSrv = srv bridge.clientFn = clientFn bridge.logger = logger diff --git a/aibridged/bridge_integration_test.go b/aibridged/bridge_integration_test.go index 7fac3a81bc82f..b10515b45dacd 100644 --- a/aibridged/bridge_integration_test.go +++ b/aibridged/bridge_integration_test.go @@ -38,7 +38,6 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/testutil" - "github.com/coder/serpent" ) var ( @@ -119,13 +118,10 @@ func TestAnthropicMessages(t *testing.T) { coderdClient := &fakeBridgeDaemonClient{} logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) - b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - Anthropic: codersdk.AIBridgeAnthropicConfig{ - BaseURL: serpent.String(srv.URL), - Key: serpent.String(sessionToken), - }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + registry := aibridged.ProviderRegistry{ + aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(srv.URL, sessionToken), + } + b, err := aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { return coderdClient, nil }, nil) require.NoError(t, err) @@ -222,13 +218,10 @@ func TestOpenAIChatCompletions(t *testing.T) { coderdClient := &fakeBridgeDaemonClient{} logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) - b, err := aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - OpenAI: codersdk.AIBridgeOpenAIConfig{ - BaseURL: serpent.String(srv.URL), - Key: serpent.String(sessionToken), - }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + registry := aibridged.ProviderRegistry{ + aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(srv.URL, sessionToken), + } + b, err := aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { return coderdClient, nil }, nil) require.NoError(t, err) @@ -290,17 +283,14 @@ func TestSimple(t *testing.T) { createRequest func(*testing.T, string, []byte) *http.Request }{ { - name: "anthropic", + name: aibridged.ProviderAnthropic, fixture: antSimple, configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { logger := testutil.Logger(t) - return aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - Anthropic: codersdk.AIBridgeAnthropicConfig{ - BaseURL: serpent.String(addr), - Key: serpent.String(sessionToken), - }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + registry := aibridged.ProviderRegistry{ + aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(addr, sessionToken), + } + return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { return client, nil }, nil) }, @@ -337,17 +327,14 @@ func TestSimple(t *testing.T) { createRequest: createAnthropicMessagesReq, }, { - name: "openai", + name: aibridged.ProviderOpenAI, fixture: oaiSimple, configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { logger := testutil.Logger(t) - return aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - OpenAI: codersdk.AIBridgeOpenAIConfig{ - BaseURL: serpent.String(addr), - Key: serpent.String(sessionToken), - }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + registry := aibridged.ProviderRegistry{ + aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(addr, sessionToken), + } + return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { return client, nil }, nil) }, @@ -496,17 +483,14 @@ func TestInjectedTool(t *testing.T) { createRequest func(*testing.T, string, []byte) *http.Request }{ { - name: "anthropic", + name: aibridged.ProviderAnthropic, fixture: antSingleInjectedTool, configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools map[string][]*aibridged.MCPTool) (*aibridged.Bridge, error) { logger := testutil.Logger(t) - return aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - Anthropic: codersdk.AIBridgeAnthropicConfig{ - BaseURL: serpent.String(addr), - Key: serpent.String(sessionToken), - }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + registry := aibridged.ProviderRegistry{ + aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(addr, sessionToken), + } + return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { return client, nil }, tools) }, @@ -556,17 +540,14 @@ func TestInjectedTool(t *testing.T) { createRequest: createAnthropicMessagesReq, }, { - name: "openai", + name: aibridged.ProviderOpenAI, fixture: oaiSingleInjectedTool, configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools map[string][]*aibridged.MCPTool) (*aibridged.Bridge, error) { logger := testutil.Logger(t) - return aibridged.NewBridge(codersdk.AIBridgeConfig{ - Daemons: 1, - OpenAI: codersdk.AIBridgeOpenAIConfig{ - BaseURL: serpent.String(addr), - Key: serpent.String(sessionToken), - }, - }, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + registry := aibridged.ProviderRegistry{ + aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(addr, sessionToken), + } + return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { return client, nil }, tools) }, diff --git a/aibridged/middleware.go b/aibridged/middleware.go index 16a25af11a7ce..34a9472ee4f62 100644 --- a/aibridged/middleware.go +++ b/aibridged/middleware.go @@ -10,8 +10,10 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" ) -type ContextKeyBridgeAPIKey struct{} -type ContextKeyBridgeUserID struct{} +type ( + ContextKeyBridgeAPIKey struct{} + ContextKeyBridgeUserID struct{} +) // AuthMiddleware extracts and validates authorization tokens for AI bridge endpoints. // It supports both Bearer tokens in Authorization headers and Coder session tokens diff --git a/aibridged/provider.go b/aibridged/provider.go index e8d5dc412e3e3..50be9c9bc2ed6 100644 --- a/aibridged/provider.go +++ b/aibridged/provider.go @@ -8,3 +8,5 @@ type Provider interface { CreateSession(w http.ResponseWriter, r *http.Request, tools ToolRegistry) (Session, error) Identifier() string } + +type ProviderRegistry map[string]Provider diff --git a/aibridged/provider_anthropic.go b/aibridged/provider_anthropic.go index 403d8e3d00779..de9983bcff460 100644 --- a/aibridged/provider_anthropic.go +++ b/aibridged/provider_anthropic.go @@ -42,5 +42,5 @@ func (p *AnthropicMessagesProvider) CreateSession(w http.ResponseWriter, r *http } func (p *AnthropicMessagesProvider) Identifier() string { - return "anthropic" + return ProviderAnthropic } diff --git a/aibridged/provider_openai.go b/aibridged/provider_openai.go index c27bf759d3b1c..ef11d7ffd4823 100644 --- a/aibridged/provider_openai.go +++ b/aibridged/provider_openai.go @@ -27,7 +27,7 @@ func NewOpenAIProvider(baseURL, key string) *OpenAIProvider { } func (p *OpenAIProvider) Identifier() string { - return "openai" + return ProviderOpenAI } func (p *OpenAIProvider) CreateSession(w http.ResponseWriter, r *http.Request, tools ToolRegistry) (Session, error) { diff --git a/aibridged/session.go b/aibridged/session.go index 00656ec344f78..6869fe0c4f9bf 100644 --- a/aibridged/session.go +++ b/aibridged/session.go @@ -19,9 +19,7 @@ type Session interface { ProcessRequest(w http.ResponseWriter, r *http.Request) error } -var ( - UnknownRoute = xerrors.New("unknown route") -) +var UnknownRoute = xerrors.New("unknown route") func NewSessionProcessor(p Provider, logger slog.Logger, client proto.DRPCAIBridgeDaemonClient, tools ToolRegistry) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/aibridged/streaming.go b/aibridged/streaming.go index 23b8c4fdd907d..c1e813f574b0a 100644 --- a/aibridged/streaming.go +++ b/aibridged/streaming.go @@ -120,8 +120,8 @@ type eventStream struct { type eventStreamProvider string const ( - openAIEventStream eventStreamProvider = "openai" - anthropicEventStream eventStreamProvider = "anthropic" + openAIEventStream eventStreamProvider = ProviderOpenAI + anthropicEventStream eventStreamProvider = ProviderAnthropic ) func newEventStream(kind eventStreamProvider) *eventStream { diff --git a/coderd/aibridge.go b/coderd/aibridge.go index 61fdd76ed998f..aa0100af3d33a 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -85,7 +85,12 @@ func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, cli api.Logger.Warn(ctx, "failed to load tools", slog.Error(err)) } - bridge, err := aibridged.NewBridge(api.DeploymentValues.AI.BridgeConfig, api.Logger.Named("ai_bridge"), clientFn, tools) + // TODO: only instantiate once. + registry := aibridged.ProviderRegistry{ + aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(api.DeploymentValues.AI.BridgeConfig.OpenAI.BaseURL.String(), api.DeploymentValues.AI.BridgeConfig.OpenAI.Key.String()), + aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(api.DeploymentValues.AI.BridgeConfig.Anthropic.BaseURL.String(), api.DeploymentValues.AI.BridgeConfig.Anthropic.Key.String()), + } + bridge, err := aibridged.NewBridge(registry, api.Logger.Named("ai_bridge"), clientFn, tools) if err != nil { return nil, xerrors.Errorf("create new bridge server: %w", err) } From d6939bedd0832ddb4d840c0aa348bf69f8cf38af Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 11 Aug 2025 15:20:45 +0200 Subject: [PATCH 57/61] correct token accumulation and tracking Signed-off-by: Danny Kopping --- aibridged/bridge_integration_test.go | 5 +- .../session_anthropic_messages_streaming.go | 46 ++++++++----------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/aibridged/bridge_integration_test.go b/aibridged/bridge_integration_test.go index b10515b45dacd..94ba4dfce2c6c 100644 --- a/aibridged/bridge_integration_test.go +++ b/aibridged/bridge_integration_test.go @@ -143,11 +143,10 @@ func TestAnthropicMessages(t *testing.T) { // Ensure the message starts and completes, at a minimum. assert.Contains(t, sp.AllEvents(), "message_start") assert.Contains(t, sp.AllEvents(), "message_stop") - require.Len(t, coderdClient.tokenUsages, 2) // One from message_start, one from message_delta. - } else { - require.Len(t, coderdClient.tokenUsages, 1) } + require.Len(t, coderdClient.tokenUsages, 1) + assert.EqualValues(t, tc.expectedInputTokens, calculateTotalInputTokens(coderdClient.tokenUsages), "input tokens miscalculated") assert.EqualValues(t, tc.expectedOutputTokens, calculateTotalOutputTokens(coderdClient.tokenUsages), "output tokens miscalculated") diff --git a/aibridged/session_anthropic_messages_streaming.go b/aibridged/session_anthropic_messages_streaming.go index dff5d45b7ba31..1b8dbfba8c940 100644 --- a/aibridged/session_anthropic_messages_streaming.go +++ b/aibridged/session_anthropic_messages_streaming.go @@ -88,6 +88,10 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter messages := s.req.BetaMessageNewParams logger := s.logger.With(slog.F("model", s.req.Model)) + // Accumulate usage across the entire streaming interaction (including tool reinvocations). + var cumulativeInputTokens int64 + var cumulativeOutputTokens int64 + isFirst := true for { newStream: @@ -134,19 +138,9 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter continue } case string(ant_constant.ValueOf[ant_constant.MessageStart]()): - // Track token usage start := event.AsMessageStart() - metadata := Metadata{ - "web_search_requests": start.Message.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": start.Message.Usage.CacheCreationInputTokens, - "cache_read_input": start.Message.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": start.Message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": start.Message.Usage.CacheCreation.Ephemeral5mInputTokens, - } - if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, start.Message.Usage.InputTokens, start.Message.Usage.OutputTokens, metadata); err != nil { - logger.Warn(ctx, "failed to track token usage", slog.Error(err)) - } - + cumulativeInputTokens += start.Message.Usage.InputTokens + cumulativeOutputTokens += start.Message.Usage.OutputTokens if !isFirst { // Don't send message_start unless first message! // We're sending multiple messages back and forth with the API, but from the client's perspective @@ -155,19 +149,8 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter } case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): delta := event.AsMessageDelta() - // Track token usage - metadata := Metadata{ - "web_search_requests": delta.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": delta.Usage.CacheCreationInputTokens, - "cache_read_input": delta.Usage.CacheReadInputTokens, - // Note: CacheCreation fields are not available in MessageDeltaUsage - "cache_ephemeral_1h_input": 0, - "cache_ephemeral_5m_input": 0, - } - if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, delta.Usage.InputTokens, delta.Usage.OutputTokens, metadata); err != nil { - logger.Warn(ctx, "failed to track token usage", slog.Error(err)) - } - + cumulativeInputTokens += delta.Usage.InputTokens + cumulativeOutputTokens += delta.Usage.OutputTokens // Don't relay message_delta events which indicate injected tool use. if len(pendingToolCalls) > 0 && s.toolMgr.GetTool(lastToolName) != nil { continue @@ -183,7 +166,6 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter // Don't send message_stop until all tools have been called. case string(ant_constant.ValueOf[ant_constant.MessageStop]()): - if len(pendingToolCalls) > 0 { // Append the whole message from this stream as context since we'll be sending a new request with the tool results. messages.Messages = append(messages.Messages, message.ToParam()) @@ -347,6 +329,18 @@ func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter } } + // Emit a single, final token usage total for this stream. + metadata := Metadata{ + "web_search_requests": message.Usage.ServerToolUse.WebSearchRequests, + "cache_creation_input": message.Usage.CacheCreationInputTokens, + "cache_read_input": message.Usage.CacheReadInputTokens, + "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, + "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, + } + if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, cumulativeInputTokens, cumulativeOutputTokens, metadata); err != nil { + logger.Warn(ctx, "failed to track token usage", slog.Error(err)) + } + var streamErr error if streamErr = stream.Err(); streamErr != nil { if isConnectionError(streamErr) { From 1ad84eb506c5b9745d0ac7cdf9b118934611f4c1 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 11 Aug 2025 18:03:57 +0200 Subject: [PATCH 58/61] provider prefixes, unhandled path reverse-proxying Signed-off-by: Danny Kopping --- aibridged/anthropic_utils.go | 15 - aibridged/bridge.go | 13 +- aibridged/bridge_integration_test.go | 106 +++- aibridged/fallthrough.go | 104 ++++ .../fixtures/anthropic/fallthrough.txtar | 64 +++ aibridged/fixtures/openai/fallthrough.txtar | 524 ++++++++++++++++++ aibridged/provider.go | 2 + aibridged/provider_anthropic.go | 44 +- aibridged/provider_openai.go | 25 +- coderd/coderd.go | 18 +- 10 files changed, 866 insertions(+), 49 deletions(-) create mode 100644 aibridged/fallthrough.go create mode 100644 aibridged/fixtures/anthropic/fallthrough.txtar create mode 100644 aibridged/fixtures/openai/fallthrough.txtar diff --git a/aibridged/anthropic_utils.go b/aibridged/anthropic_utils.go index 3802d8f1d225f..ae9f59c82fb0a 100644 --- a/aibridged/anthropic_utils.go +++ b/aibridged/anthropic_utils.go @@ -3,26 +3,11 @@ package aibridged import ( "encoding/json" "errors" - "os" "github.com/anthropics/anthropic-sdk-go" - "github.com/anthropics/anthropic-sdk-go/option" ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" ) -// newAnthropicClient creates an Anthropic client with the given base URL and API key. -func newAnthropicClient(baseURL, key string, opts ...option.RequestOption) anthropic.Client { - if key == "" { - key = os.Getenv("ANTHROPIC_API_KEY") - } - opts = append(opts, option.WithAPIKey(key)) - if baseURL != "" { - opts = append(opts, option.WithBaseURL(baseURL)) - } - - return anthropic.NewClient(opts...) -} - func getAnthropicErrorResponse(err error) *AnthropicErrorResponse { var apierr *anthropic.Error if !errors.As(err, &apierr) { diff --git a/aibridged/bridge.go b/aibridged/bridge.go index e5f14e5acecd4..5faf52caf9597 100644 --- a/aibridged/bridge.go +++ b/aibridged/bridge.go @@ -1,6 +1,7 @@ package aibridged import ( + "fmt" "net/http" "time" @@ -17,8 +18,8 @@ const ( ) var providerRoutes = map[string][]string{ - ProviderOpenAI: {"/v1/chat/completions"}, - ProviderAnthropic: {"/v1/messages"}, + ProviderOpenAI: {"/openai/v1/chat/completions"}, + ProviderAnthropic: {"/anthropic/v1/messages"}, } // Bridge is responsible for proxying requests to upstream AI providers. @@ -54,9 +55,17 @@ func NewBridge(registry ProviderRegistry, logger slog.Logger, clientFn func() (p // Unknown provider identifier; skip. continue } + // Add the known provider-specific routes. for _, path := range routes { mux.HandleFunc(path, NewSessionProcessor(provider, logger, drpcClient, tools)) } + + // Implement a catch-all route: any requests which fall through to this will be reverse-proxied to the upstream. + // Note: net/http ServeMux uses subtree matching when the pattern ends with a trailing slash. + fallthroughRoute := fmt.Sprintf("/%s/", ident) + mux.Handle(fallthroughRoute, http.StripPrefix(fallthroughRoute, + newFallthroughRouter(provider, logger.Named(fmt.Sprintf("%s.fallthrough", ident)))), + ) } srv := &http.Server{ diff --git a/aibridged/bridge_integration_test.go b/aibridged/bridge_integration_test.go index 94ba4dfce2c6c..31bea35a6f3c3 100644 --- a/aibridged/bridge_integration_test.go +++ b/aibridged/bridge_integration_test.go @@ -56,6 +56,12 @@ var ( //go:embed fixtures/openai/simple.txtar oaiSimple []byte + + //go:embed fixtures/anthropic/fallthrough.txtar + antFallthrough []byte + + //go:embed fixtures/openai/fallthrough.txtar + oaiFallthrough []byte ) const ( @@ -64,6 +70,7 @@ const ( fixtureNonStreamingResponse = "non-streaming" fixtureStreamingToolResponse = "streaming/tool-call" fixtureNonStreamingToolResponse = "non-streaming/tool-call" + fixtureResponse = "response" ) func TestAnthropicMessages(t *testing.T) { @@ -444,6 +451,99 @@ func TestSimple(t *testing.T) { } } +func TestFallthrough(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + sessionToken := getSessionToken(t, client) + + testCases := []struct { + provider string + fixture []byte + authHeaderName string + configureFunc func(string, proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) + }{ + { + provider: aibridged.ProviderAnthropic, + fixture: antFallthrough, + configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { + logger := testutil.Logger(t) + registry := aibridged.ProviderRegistry{ + aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(addr, sessionToken), + } + return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + return client, nil + }, nil) + }, + }, + { + provider: aibridged.ProviderOpenAI, + fixture: oaiFallthrough, + configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { + logger := testutil.Logger(t) + registry := aibridged.ProviderRegistry{ + aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(addr, sessionToken), + } + return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { + return client, nil + }, nil) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.provider, func(t *testing.T) { + t.Parallel() + + arc := txtar.Parse(tc.fixture) + t.Logf("%s: %s", t.Name(), arc.Comment) + + files := filesMap(arc) + require.Contains(t, files, fixtureResponse) + + respBody := files[fixtureResponse] + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/models" { + t.Errorf("unexpected request path: %q", r.URL.Path) + t.FailNow() + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(respBody) + })) + t.Cleanup(upstream.Close) + + coderdClient := &fakeBridgeDaemonClient{} + + bridge, err := tc.configureFunc(upstream.URL, coderdClient) + require.NoError(t, err) + + bridgeSrv := httptest.NewServer(bridge.Handler()) + t.Cleanup(bridgeSrv.Close) + + req, err := http.NewRequestWithContext(t.Context(), "GET", fmt.Sprintf("%s/%s/v1/models", bridgeSrv.URL, tc.provider), nil) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + + gotBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + // Compare JSON bodies for semantic equality. + var got any + var exp any + require.NoError(t, json.Unmarshal(gotBytes, &got)) + require.NoError(t, json.Unmarshal(respBody, &exp)) + require.EqualValues(t, exp, got) + }) + } +} + // setupMCPToolsForTest creates a mock MCP server, initializes the MCP bridge, and returns the tools func setupMCPToolsForTest(t *testing.T) map[string][]*aibridged.MCPTool { t.Helper() @@ -722,7 +822,7 @@ func filesMap(archive *txtar.Archive) archiveFileMap { func createAnthropicMessagesReq(t *testing.T, baseURL string, input []byte) *http.Request { t.Helper() - req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/v1/messages", bytes.NewReader(input)) + req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/anthropic/v1/messages", bytes.NewReader(input)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") @@ -732,7 +832,7 @@ func createAnthropicMessagesReq(t *testing.T, baseURL string, input []byte) *htt func createOpenAIChatCompletionsReq(t *testing.T, baseURL string, input []byte) *http.Request { t.Helper() - req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/v1/chat/completions", bytes.NewReader(input)) + req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/openai/v1/chat/completions", bytes.NewReader(input)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") @@ -816,6 +916,8 @@ func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap, resp return } })) + t.Cleanup(srv.Close) + srv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx } diff --git a/aibridged/fallthrough.go b/aibridged/fallthrough.go new file mode 100644 index 0000000000000..c376ddb838d4a --- /dev/null +++ b/aibridged/fallthrough.go @@ -0,0 +1,104 @@ +package aibridged + +import ( + "net" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "cdr.dev/slog" +) + +// fallthroughRouter is a simple reverse-proxy implementation to enable +type fallthroughRouter struct { + provider Provider + logger slog.Logger +} + +func newFallthroughRouter(provider Provider, logger slog.Logger) *fallthroughRouter { + return &fallthroughRouter{ + provider: provider, + logger: logger, + } +} + +func (f *fallthroughRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + upURL, err := url.Parse(f.provider.BaseURL()) + if err != nil { + f.logger.Error(r.Context(), "failed to parse provider base URL", slog.Error(err)) + http.Error(w, "request error", http.StatusBadGateway) + return + } + + // Build a reverse proxy to the upstream. + proxy := &httputil.ReverseProxy{ + Director: func(req *http.Request) { + // Set scheme/host to upstream. + req.URL.Scheme = upURL.Scheme + req.URL.Host = upURL.Host + + // Preserve the stripped path from the incoming request and ensure leading slash. + p := r.URL.Path + if len(p) == 0 || p[0] != '/' { + p = "/" + p + } + req.URL.Path = p + req.URL.RawPath = "" + + // Preserve query string. + req.URL.RawQuery = r.URL.RawQuery + + // Set Host header for upstream. + req.Host = upURL.Host + + // Copy headers from client. + req.Header = r.Header.Clone() + + // Standard proxy headers. + host, _, herr := net.SplitHostPort(r.RemoteAddr) + if herr != nil { + host = r.RemoteAddr + } + if prior := req.Header.Get("X-Forwarded-For"); prior != "" { + req.Header.Set("X-Forwarded-For", prior+", "+host) + } else { + req.Header.Set("X-Forwarded-For", host) + } + req.Header.Set("X-Forwarded-Host", r.Host) + if r.TLS != nil { + req.Header.Set("X-Forwarded-Proto", "https") + } else { + req.Header.Set("X-Forwarded-Proto", "http") + } + // Avoid default Go user-agent if none provided. + if _, ok := req.Header["User-Agent"]; !ok { + req.Header.Set("User-Agent", "aibridged") // TODO: use build tag. + } + + // Inject provider auth. + switch f.provider.(type) { + case *OpenAIProvider: + req.Header.Set("Authorization", "Bearer "+f.provider.Key()) + case *AnthropicMessagesProvider: + req.Header.Set("x-api-key", f.provider.Key()) + } + }, + ErrorHandler: func(rw http.ResponseWriter, req *http.Request, e error) { + f.logger.Error(req.Context(), "reverse proxy error", slog.Error(e), slog.F("path", req.URL.Path)) + http.Error(rw, "upstream proxy error", http.StatusBadGateway) + }, + } + + // Transport tuned for streaming (no response header timeout). + proxy.Transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + + proxy.ServeHTTP(w, r) +} diff --git a/aibridged/fixtures/anthropic/fallthrough.txtar b/aibridged/fixtures/anthropic/fallthrough.txtar new file mode 100644 index 0000000000000..6d9801d9620c5 --- /dev/null +++ b/aibridged/fixtures/anthropic/fallthrough.txtar @@ -0,0 +1,64 @@ +API endpoints not explicitly handled will fallthrough to upstream via reverse-proxy. + +-- response -- +{ + "data": [ + { + "type": "model", + "id": "claude-opus-4-1-20250805", + "display_name": "Claude Opus 4.1", + "created_at": "2025-08-05T00:00:00Z" + }, + { + "type": "model", + "id": "claude-opus-4-20250514", + "display_name": "Claude Opus 4", + "created_at": "2025-05-22T00:00:00Z" + }, + { + "type": "model", + "id": "claude-sonnet-4-20250514", + "display_name": "Claude Sonnet 4", + "created_at": "2025-05-22T00:00:00Z" + }, + { + "type": "model", + "id": "claude-3-7-sonnet-20250219", + "display_name": "Claude Sonnet 3.7", + "created_at": "2025-02-24T00:00:00Z" + }, + { + "type": "model", + "id": "claude-3-5-sonnet-20241022", + "display_name": "Claude Sonnet 3.5 (New)", + "created_at": "2024-10-22T00:00:00Z" + }, + { + "type": "model", + "id": "claude-3-5-haiku-20241022", + "display_name": "Claude Haiku 3.5", + "created_at": "2024-10-22T00:00:00Z" + }, + { + "type": "model", + "id": "claude-3-5-sonnet-20240620", + "display_name": "Claude Sonnet 3.5 (Old)", + "created_at": "2024-06-20T00:00:00Z" + }, + { + "type": "model", + "id": "claude-3-haiku-20240307", + "display_name": "Claude Haiku 3", + "created_at": "2024-03-07T00:00:00Z" + }, + { + "type": "model", + "id": "claude-3-opus-20240229", + "display_name": "Claude Opus 3", + "created_at": "2024-02-29T00:00:00Z" + } + ], + "has_more": false, + "first_id": "claude-opus-4-1-20250805", + "last_id": "claude-3-opus-20240229" +} diff --git a/aibridged/fixtures/openai/fallthrough.txtar b/aibridged/fixtures/openai/fallthrough.txtar new file mode 100644 index 0000000000000..09812cb6e4a33 --- /dev/null +++ b/aibridged/fixtures/openai/fallthrough.txtar @@ -0,0 +1,524 @@ +API endpoints not explicitly handled will fallthrough to upstream via reverse-proxy. + +-- response -- +{ + "object": "list", + "data": [ + { + "id": "gpt-4-0613", + "object": "model", + "created": 1686588896, + "owned_by": "openai" + }, + { + "id": "gpt-4", + "object": "model", + "created": 1687882411, + "owned_by": "openai" + }, + { + "id": "gpt-3.5-turbo", + "object": "model", + "created": 1677610602, + "owned_by": "openai" + }, + { + "id": "gpt-5-nano", + "object": "model", + "created": 1754426384, + "owned_by": "system" + }, + { + "id": "gpt-5", + "object": "model", + "created": 1754425777, + "owned_by": "system" + }, + { + "id": "gpt-5-mini-2025-08-07", + "object": "model", + "created": 1754425867, + "owned_by": "system" + }, + { + "id": "gpt-5-mini", + "object": "model", + "created": 1754425928, + "owned_by": "system" + }, + { + "id": "gpt-5-nano-2025-08-07", + "object": "model", + "created": 1754426303, + "owned_by": "system" + }, + { + "id": "davinci-002", + "object": "model", + "created": 1692634301, + "owned_by": "system" + }, + { + "id": "babbage-002", + "object": "model", + "created": 1692634615, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-instruct", + "object": "model", + "created": 1692901427, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-instruct-0914", + "object": "model", + "created": 1694122472, + "owned_by": "system" + }, + { + "id": "dall-e-3", + "object": "model", + "created": 1698785189, + "owned_by": "system" + }, + { + "id": "dall-e-2", + "object": "model", + "created": 1698798177, + "owned_by": "system" + }, + { + "id": "gpt-4-1106-preview", + "object": "model", + "created": 1698957206, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-1106", + "object": "model", + "created": 1698959748, + "owned_by": "system" + }, + { + "id": "tts-1-hd", + "object": "model", + "created": 1699046015, + "owned_by": "system" + }, + { + "id": "tts-1-1106", + "object": "model", + "created": 1699053241, + "owned_by": "system" + }, + { + "id": "tts-1-hd-1106", + "object": "model", + "created": 1699053533, + "owned_by": "system" + }, + { + "id": "text-embedding-3-small", + "object": "model", + "created": 1705948997, + "owned_by": "system" + }, + { + "id": "text-embedding-3-large", + "object": "model", + "created": 1705953180, + "owned_by": "system" + }, + { + "id": "gpt-4-0125-preview", + "object": "model", + "created": 1706037612, + "owned_by": "system" + }, + { + "id": "gpt-4-turbo-preview", + "object": "model", + "created": 1706037777, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-0125", + "object": "model", + "created": 1706048358, + "owned_by": "system" + }, + { + "id": "gpt-4-turbo", + "object": "model", + "created": 1712361441, + "owned_by": "system" + }, + { + "id": "gpt-4-turbo-2024-04-09", + "object": "model", + "created": 1712601677, + "owned_by": "system" + }, + { + "id": "gpt-4o", + "object": "model", + "created": 1715367049, + "owned_by": "system" + }, + { + "id": "gpt-4o-2024-05-13", + "object": "model", + "created": 1715368132, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-2024-07-18", + "object": "model", + "created": 1721172717, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini", + "object": "model", + "created": 1721172741, + "owned_by": "system" + }, + { + "id": "gpt-4o-2024-08-06", + "object": "model", + "created": 1722814719, + "owned_by": "system" + }, + { + "id": "chatgpt-4o-latest", + "object": "model", + "created": 1723515131, + "owned_by": "system" + }, + { + "id": "o1-mini-2024-09-12", + "object": "model", + "created": 1725648979, + "owned_by": "system" + }, + { + "id": "o1-mini", + "object": "model", + "created": 1725649008, + "owned_by": "system" + }, + { + "id": "gpt-4o-realtime-preview-2024-10-01", + "object": "model", + "created": 1727131766, + "owned_by": "system" + }, + { + "id": "gpt-4o-audio-preview-2024-10-01", + "object": "model", + "created": 1727389042, + "owned_by": "system" + }, + { + "id": "gpt-4o-audio-preview", + "object": "model", + "created": 1727460443, + "owned_by": "system" + }, + { + "id": "gpt-4o-realtime-preview", + "object": "model", + "created": 1727659998, + "owned_by": "system" + }, + { + "id": "omni-moderation-latest", + "object": "model", + "created": 1731689265, + "owned_by": "system" + }, + { + "id": "omni-moderation-2024-09-26", + "object": "model", + "created": 1732734466, + "owned_by": "system" + }, + { + "id": "gpt-4o-realtime-preview-2024-12-17", + "object": "model", + "created": 1733945430, + "owned_by": "system" + }, + { + "id": "gpt-4o-audio-preview-2024-12-17", + "object": "model", + "created": 1734034239, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-realtime-preview-2024-12-17", + "object": "model", + "created": 1734112601, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-audio-preview-2024-12-17", + "object": "model", + "created": 1734115920, + "owned_by": "system" + }, + { + "id": "o1-2024-12-17", + "object": "model", + "created": 1734326976, + "owned_by": "system" + }, + { + "id": "o1", + "object": "model", + "created": 1734375816, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-realtime-preview", + "object": "model", + "created": 1734387380, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-audio-preview", + "object": "model", + "created": 1734387424, + "owned_by": "system" + }, + { + "id": "o3-mini", + "object": "model", + "created": 1737146383, + "owned_by": "system" + }, + { + "id": "o3-mini-2025-01-31", + "object": "model", + "created": 1738010200, + "owned_by": "system" + }, + { + "id": "gpt-4o-2024-11-20", + "object": "model", + "created": 1739331543, + "owned_by": "system" + }, + { + "id": "gpt-4o-search-preview-2025-03-11", + "object": "model", + "created": 1741388170, + "owned_by": "system" + }, + { + "id": "gpt-4o-search-preview", + "object": "model", + "created": 1741388720, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-search-preview-2025-03-11", + "object": "model", + "created": 1741390858, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-search-preview", + "object": "model", + "created": 1741391161, + "owned_by": "system" + }, + { + "id": "gpt-4o-transcribe", + "object": "model", + "created": 1742068463, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-transcribe", + "object": "model", + "created": 1742068596, + "owned_by": "system" + }, + { + "id": "o1-pro-2025-03-19", + "object": "model", + "created": 1742251504, + "owned_by": "system" + }, + { + "id": "o1-pro", + "object": "model", + "created": 1742251791, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-tts", + "object": "model", + "created": 1742403959, + "owned_by": "system" + }, + { + "id": "o3-2025-04-16", + "object": "model", + "created": 1744133301, + "owned_by": "system" + }, + { + "id": "o4-mini-2025-04-16", + "object": "model", + "created": 1744133506, + "owned_by": "system" + }, + { + "id": "o3", + "object": "model", + "created": 1744225308, + "owned_by": "system" + }, + { + "id": "o4-mini", + "object": "model", + "created": 1744225351, + "owned_by": "system" + }, + { + "id": "gpt-4.1-2025-04-14", + "object": "model", + "created": 1744315746, + "owned_by": "system" + }, + { + "id": "gpt-4.1", + "object": "model", + "created": 1744316542, + "owned_by": "system" + }, + { + "id": "gpt-4.1-mini-2025-04-14", + "object": "model", + "created": 1744317547, + "owned_by": "system" + }, + { + "id": "gpt-4.1-mini", + "object": "model", + "created": 1744318173, + "owned_by": "system" + }, + { + "id": "gpt-4.1-nano-2025-04-14", + "object": "model", + "created": 1744321025, + "owned_by": "system" + }, + { + "id": "gpt-4.1-nano", + "object": "model", + "created": 1744321707, + "owned_by": "system" + }, + { + "id": "gpt-image-1", + "object": "model", + "created": 1745517030, + "owned_by": "system" + }, + { + "id": "codex-mini-latest", + "object": "model", + "created": 1746673257, + "owned_by": "system" + }, + { + "id": "o3-pro", + "object": "model", + "created": 1748475349, + "owned_by": "system" + }, + { + "id": "gpt-4o-realtime-preview-2025-06-03", + "object": "model", + "created": 1748907838, + "owned_by": "system" + }, + { + "id": "gpt-4o-audio-preview-2025-06-03", + "object": "model", + "created": 1748908498, + "owned_by": "system" + }, + { + "id": "o3-pro-2025-06-10", + "object": "model", + "created": 1749166761, + "owned_by": "system" + }, + { + "id": "o4-mini-deep-research", + "object": "model", + "created": 1749685485, + "owned_by": "system" + }, + { + "id": "o3-deep-research", + "object": "model", + "created": 1749840121, + "owned_by": "system" + }, + { + "id": "o3-deep-research-2025-06-26", + "object": "model", + "created": 1750865219, + "owned_by": "system" + }, + { + "id": "o4-mini-deep-research-2025-06-26", + "object": "model", + "created": 1750866121, + "owned_by": "system" + }, + { + "id": "gpt-5-chat-latest", + "object": "model", + "created": 1754073306, + "owned_by": "system" + }, + { + "id": "gpt-5-2025-08-07", + "object": "model", + "created": 1754075360, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-16k", + "object": "model", + "created": 1683758102, + "owned_by": "openai-internal" + }, + { + "id": "tts-1", + "object": "model", + "created": 1681940951, + "owned_by": "openai-internal" + }, + { + "id": "whisper-1", + "object": "model", + "created": 1677532384, + "owned_by": "openai-internal" + }, + { + "id": "text-embedding-ada-002", + "object": "model", + "created": 1671217299, + "owned_by": "openai-internal" + } + ] +} diff --git a/aibridged/provider.go b/aibridged/provider.go index 50be9c9bc2ed6..7666846070117 100644 --- a/aibridged/provider.go +++ b/aibridged/provider.go @@ -7,6 +7,8 @@ import ( type Provider interface { CreateSession(w http.ResponseWriter, r *http.Request, tools ToolRegistry) (Session, error) Identifier() string + BaseURL() string + Key() string } type ProviderRegistry map[string]Provider diff --git a/aibridged/provider_anthropic.go b/aibridged/provider_anthropic.go index de9983bcff460..1f8aef02f3558 100644 --- a/aibridged/provider_anthropic.go +++ b/aibridged/provider_anthropic.go @@ -4,7 +4,10 @@ import ( "encoding/json" "io" "net/http" + "os" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" "golang.org/x/xerrors" ) @@ -17,6 +20,13 @@ type AnthropicMessagesProvider struct { } func NewAnthropicMessagesProvider(baseURL, key string) *AnthropicMessagesProvider { + if baseURL == "" { + baseURL = "https://api.anthropic.com/" + } + if key == "" { + key = os.Getenv("ANTHROPIC_API_KEY") + } + return &AnthropicMessagesProvider{ baseURL: baseURL, key: key, @@ -29,18 +39,38 @@ func (p *AnthropicMessagesProvider) CreateSession(w http.ResponseWriter, r *http return nil, xerrors.Errorf("read body: %w", err) } - var req BetaMessageNewParamsWrapper - if err := json.Unmarshal(payload, &req); err != nil { - return nil, xerrors.Errorf("failed to unmarshal request: %w", err) - } + switch r.URL.Path { + case "/anthropic/v1/messages": + var req BetaMessageNewParamsWrapper + if err := json.Unmarshal(payload, &req); err != nil { + return nil, xerrors.Errorf("failed to unmarshal request: %w", err) + } + + if req.Stream { + return NewAnthropicMessagesStreamingSession(&req, p.baseURL, p.key), nil + } - if req.Stream { - return NewAnthropicMessagesStreamingSession(&req, p.baseURL, p.key), nil + return NewAnthropicMessagesBlockingSession(&req, p.baseURL, p.key), nil } - return NewAnthropicMessagesBlockingSession(&req, p.baseURL, p.key), nil + return nil, UnknownRoute } func (p *AnthropicMessagesProvider) Identifier() string { return ProviderAnthropic } + +func (p *AnthropicMessagesProvider) BaseURL() string { + return p.baseURL +} + +func (p *AnthropicMessagesProvider) Key() string { + return p.key +} + +func newAnthropicClient(baseURL, key string, opts ...option.RequestOption) anthropic.Client { + opts = append(opts, option.WithAPIKey(key)) + opts = append(opts, option.WithBaseURL(baseURL)) + + return anthropic.NewClient(opts...) +} diff --git a/aibridged/provider_openai.go b/aibridged/provider_openai.go index ef11d7ffd4823..21abd24e0dcb0 100644 --- a/aibridged/provider_openai.go +++ b/aibridged/provider_openai.go @@ -20,6 +20,14 @@ type OpenAIProvider struct { } func NewOpenAIProvider(baseURL, key string) *OpenAIProvider { + if baseURL == "" { + baseURL = "https://api.openai.com/v1/" + } + + if key == "" { + key = os.Getenv("OPENAI_API_KEY") + } + return &OpenAIProvider{ baseURL: baseURL, key: key, @@ -37,7 +45,7 @@ func (p *OpenAIProvider) CreateSession(w http.ResponseWriter, r *http.Request, t } switch r.URL.Path { - case "/v1/chat/completions": + case "/openai/v1/chat/completions": var req ChatCompletionNewParamsWrapper if err := json.Unmarshal(payload, &req); err != nil { return nil, xerrors.Errorf("unmarshal request body: %w", err) @@ -53,15 +61,18 @@ func (p *OpenAIProvider) CreateSession(w http.ResponseWriter, r *http.Request, t return nil, UnknownRoute } +func (p *OpenAIProvider) BaseURL() string { + return p.baseURL +} + +func (p *OpenAIProvider) Key() string { + return p.key +} + func newOpenAIClient(baseURL, key string) openai.Client { var opts []option.RequestOption - if key == "" { - key = os.Getenv("OPENAI_API_KEY") - } opts = append(opts, option.WithAPIKey(key)) - if baseURL != "" { - opts = append(opts, option.WithBaseURL(baseURL)) - } + opts = append(opts, option.WithBaseURL(baseURL)) return openai.NewClient(opts...) } diff --git a/coderd/coderd.go b/coderd/coderd.go index 92d19f87b2114..e6b67c5240de6 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1567,22 +1567,8 @@ func New(options *Options) *API { }) r.Route("/aibridge", func(r chi.Router) { r.Use(aibridged.AuthMiddleware(api.Database)) - r.Post("/v1/chat/completions", api.bridgeAIRequest) - r.Get("/v1/models", func(rw http.ResponseWriter, r *http.Request) { - // TODO: reverse-proxy blindly to upstream, or implement using policies to control available models. - httpapi.Write(context.Background(), rw, http.StatusOK, map[string]any{ - "object": "list", - "data": []map[string]any{ - { - "id": "gpt-4-0613", - "object": "model", - "created": 1686588896, - "owned_by": "openai", - }, - }, - }) - }) - r.Post("/v1/messages", api.bridgeAIRequest) + r.HandleFunc("/openai/*", api.bridgeAIRequest) + r.HandleFunc("/anthropic/*", api.bridgeAIRequest) }) }) From f351b12ae5e9c76abecb1231d3ef8136c3734240 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 12 Aug 2025 13:40:13 +0200 Subject: [PATCH 59/61] fix possible panic Signed-off-by: Danny Kopping --- aibridged/anthropic_utils.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aibridged/anthropic_utils.go b/aibridged/anthropic_utils.go index ae9f59c82fb0a..06f40e433570f 100644 --- a/aibridged/anthropic_utils.go +++ b/aibridged/anthropic_utils.go @@ -15,6 +15,7 @@ func getAnthropicErrorResponse(err error) *AnthropicErrorResponse { } msg := apierr.Error() + typ := string(ant_constant.ValueOf[ant_constant.APIError]()) var detail *anthropic.BetaAPIError if field, ok := apierr.JSON.ExtraFields["error"]; ok { @@ -22,13 +23,14 @@ func getAnthropicErrorResponse(err error) *AnthropicErrorResponse { } if detail != nil { msg = detail.Message + typ = string(detail.Type) } return &AnthropicErrorResponse{ BetaErrorResponse: &anthropic.BetaErrorResponse{ Error: anthropic.BetaErrorUnion{ Message: msg, - Type: string(detail.Type), + Type: typ, }, Type: ant_constant.ValueOf[ant_constant.Error](), }, From bd6676ab98acc9fb0f033a2d581ec3b47e574607 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 12 Aug 2025 17:04:11 +0200 Subject: [PATCH 60/61] Split into own lib Signed-off-by: Danny Kopping --- Makefile | 9 - aibridged/aibridged.go | 30 +- aibridged/anthropic.go | 227 ---- aibridged/anthropic_utils.go | 45 - aibridged/bridge.go | 98 -- aibridged/bridge_integration_test.go | 1014 ----------------- aibridged/fallthrough.go | 104 -- aibridged/fixtures/README.md | 25 - .../fixtures/anthropic/fallthrough.txtar | 64 -- aibridged/fixtures/anthropic/simple.txtar | 139 --- .../anthropic/single_builtin_tool.txtar | 127 --- .../anthropic/single_injected_tool.txtar | 163 --- aibridged/fixtures/openai/fallthrough.txtar | 524 --------- aibridged/fixtures/openai/simple.txtar | 536 --------- .../fixtures/openai/single_builtin_tool.txtar | 102 -- .../openai/single_injected_tool.txtar | 294 ----- aibridged/mcp.go | 170 --- aibridged/middleware.go | 12 +- aibridged/openai.go | 96 -- aibridged/proto/aibridged.pb.go | 761 ------------- aibridged/proto/aibridged.proto | 50 - aibridged/proto/aibridged_drpc.pb.go | 231 ---- aibridged/provider.go | 14 - aibridged/provider_anthropic.go | 76 -- aibridged/provider_openai.go | 78 -- aibridged/session.go | 61 - aibridged/session_anthropic_messages_base.go | 77 -- .../session_anthropic_messages_blocking.go | 280 ----- .../session_anthropic_messages_streaming.go | 378 ------ aibridged/session_openai_chat_base.go | 76 -- aibridged/session_openai_chat_blocking.go | 199 ---- aibridged/session_openai_chat_streaming.go | 241 ---- aibridged/sse_parser.go | 124 -- aibridged/streaming.go | 217 ---- aibridged/tool_manager.go | 78 -- aibridged/tracker.go | 96 -- cli/server.go | 10 +- coderd/aibridge.go | 21 +- coderd/aibridgedserver/aibridgedserver.go | 10 +- coderd/coderd.go | 13 +- go.mod | 22 +- go.sum | 16 +- 42 files changed, 69 insertions(+), 6839 deletions(-) delete mode 100644 aibridged/anthropic.go delete mode 100644 aibridged/anthropic_utils.go delete mode 100644 aibridged/bridge.go delete mode 100644 aibridged/bridge_integration_test.go delete mode 100644 aibridged/fallthrough.go delete mode 100644 aibridged/fixtures/README.md delete mode 100644 aibridged/fixtures/anthropic/fallthrough.txtar delete mode 100644 aibridged/fixtures/anthropic/simple.txtar delete mode 100644 aibridged/fixtures/anthropic/single_builtin_tool.txtar delete mode 100644 aibridged/fixtures/anthropic/single_injected_tool.txtar delete mode 100644 aibridged/fixtures/openai/fallthrough.txtar delete mode 100644 aibridged/fixtures/openai/simple.txtar delete mode 100644 aibridged/fixtures/openai/single_builtin_tool.txtar delete mode 100644 aibridged/fixtures/openai/single_injected_tool.txtar delete mode 100644 aibridged/mcp.go delete mode 100644 aibridged/openai.go delete mode 100644 aibridged/proto/aibridged.pb.go delete mode 100644 aibridged/proto/aibridged.proto delete mode 100644 aibridged/proto/aibridged_drpc.pb.go delete mode 100644 aibridged/provider.go delete mode 100644 aibridged/provider_anthropic.go delete mode 100644 aibridged/provider_openai.go delete mode 100644 aibridged/session.go delete mode 100644 aibridged/session_anthropic_messages_base.go delete mode 100644 aibridged/session_anthropic_messages_blocking.go delete mode 100644 aibridged/session_anthropic_messages_streaming.go delete mode 100644 aibridged/session_openai_chat_base.go delete mode 100644 aibridged/session_openai_chat_blocking.go delete mode 100644 aibridged/session_openai_chat_streaming.go delete mode 100644 aibridged/sse_parser.go delete mode 100644 aibridged/streaming.go delete mode 100644 aibridged/tool_manager.go delete mode 100644 aibridged/tracker.go diff --git a/Makefile b/Makefile index d41bb7227401c..bd3f04a4874cd 100644 --- a/Makefile +++ b/Makefile @@ -619,7 +619,6 @@ GEN_FILES := \ provisionersdk/proto/provisioner.pb.go \ provisionerd/proto/provisionerd.pb.go \ vpn/vpn.pb.go \ - aibridged/proto/aibridged.pb.go \ $(DB_GEN_FILES) \ $(SITE_GEN_FILES) \ coderd/rbac/object_gen.go \ @@ -779,14 +778,6 @@ vpn/vpn.pb.go: vpn/vpn.proto --go_opt=paths=source_relative \ ./vpn/vpn.proto -aibridged/proto/aibridged.pb.go: aibridged/proto/aibridged.proto - protoc \ - --go_out=. \ - --go_opt=paths=source_relative \ - --go-drpc_out=. \ - --go-drpc_opt=paths=source_relative \ - ./aibridged/proto/aibridged.proto - site/src/api/typesGenerated.ts: site/node_modules/.installed $(wildcard scripts/apitypings/*) $(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go') # -C sets the directory for the go run command go run -C ./scripts/apitypings main.go > $@ diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 98ae8e3f031e2..8f43db08c8715 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -15,15 +15,15 @@ import ( "cdr.dev/slog" "github.com/coder/retry" - "github.com/coder/coder/v2/aibridged/proto" + "github.com/coder/aibridge/proto" "github.com/coder/coder/v2/codersdk" ) -type Dialer func(ctx context.Context) (proto.DRPCAIBridgeDaemonClient, error) +type Dialer func(ctx context.Context) (proto.DRPCStoreClient, error) type Server struct { clientDialer Dialer - clientCh chan proto.DRPCAIBridgeDaemonClient + clientCh chan proto.DRPCStoreClient logger slog.Logger wg sync.WaitGroup @@ -50,9 +50,9 @@ type Server struct { shuttingDownCh chan struct{} } -var _ proto.DRPCAIBridgeDaemonServer = &Server{} +var _ proto.DRPCStoreServer = &Server{} -func New(rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) { +func New(rpcDialer Dialer, logger slog.Logger) (*Server, error) { if rpcDialer == nil { return nil, xerrors.Errorf("nil rpcDialer given") } @@ -61,7 +61,7 @@ func New(rpcDialer Dialer, httpAddr string, logger slog.Logger) (*Server, error) daemon := &Server{ logger: logger, clientDialer: rpcDialer, - clientCh: make(chan proto.DRPCAIBridgeDaemonClient), + clientCh: make(chan proto.DRPCStoreClient), closeContext: ctx, closeCancel: cancel, closedCh: make(chan struct{}), @@ -137,7 +137,7 @@ connectLoop: } } -func (s *Server) Client() (proto.DRPCAIBridgeDaemonClient, error) { +func (s *Server) Client() (proto.DRPCStoreClient, error) { select { case <-s.closeContext.Done(): return nil, xerrors.New("context closed") @@ -149,9 +149,9 @@ func (s *Server) Client() (proto.DRPCAIBridgeDaemonClient, error) { } } -func (s *Server) StartSession(ctx context.Context, in *proto.StartSessionRequest) (*proto.StartSessionResponse, error) { - out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.StartSessionResponse, error) { - return client.StartSession(ctx, in) +func (s *Server) StoreSession(ctx context.Context, in *proto.StoreSessionRequest) (*proto.StoreSessionResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCStoreClient) (*proto.StoreSessionResponse, error) { + return client.StoreSession(ctx, in) }) if err != nil { return nil, err @@ -160,7 +160,7 @@ func (s *Server) StartSession(ctx context.Context, in *proto.StartSessionRequest } func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { - out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackTokenUsageResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCStoreClient) (*proto.TrackTokenUsageResponse, error) { return client.TrackTokenUsage(ctx, in) }) if err != nil { @@ -170,7 +170,7 @@ func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageR } func (s *Server) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptRequest) (*proto.TrackUserPromptResponse, error) { - out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackUserPromptResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCStoreClient) (*proto.TrackUserPromptResponse, error) { return client.TrackUserPrompt(ctx, in) }) if err != nil { @@ -180,7 +180,7 @@ func (s *Server) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptR } func (s *Server) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageRequest) (*proto.TrackToolUsageResponse, error) { - out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCAIBridgeDaemonClient) (*proto.TrackToolUsageResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCStoreClient) (*proto.TrackToolUsageResponse, error) { return client.TrackToolUsage(ctx, in) }) if err != nil { @@ -202,8 +202,8 @@ func retryable(err error) bool { // expires. // NOTE: mostly copypasta from provisionerd; might be work abstracting. func clientDoWithRetries[T any](ctx context.Context, - getClient func() (proto.DRPCAIBridgeDaemonClient, error), - f func(context.Context, proto.DRPCAIBridgeDaemonClient) (T, error), + getClient func() (proto.DRPCStoreClient, error), + f func(context.Context, proto.DRPCStoreClient) (T, error), ) (ret T, _ error) { for retrier := retry.New(25*time.Millisecond, 5*time.Second); retrier.Wait(ctx); { var empty T diff --git a/aibridged/anthropic.go b/aibridged/anthropic.go deleted file mode 100644 index c2b52067d6579..0000000000000 --- a/aibridged/anthropic.go +++ /dev/null @@ -1,227 +0,0 @@ -package aibridged - -import ( - "encoding/json" - "regexp" - "strings" - - "github.com/anthropics/anthropic-sdk-go" - ant_param "github.com/anthropics/anthropic-sdk-go/packages/param" - "github.com/tidwall/gjson" - "golang.org/x/xerrors" - "tailscale.com/types/ptr" -) - -type streamer interface { - UseStreaming() bool -} - -// ConvertStringContentToArrayTest exports the function for testing -func ConvertStringContentToArrayTest(raw []byte) ([]byte, error) { - return convertStringContentToArray(raw) -} - -// convertStringContentToArray converts string content to array format for Anthropic messages. -// https://docs.anthropic.com/en/api/messages#body-messages -// -// Each input message content may be either a single string or an array of content blocks, where each block has a -// specific type. Using a string for content is shorthand for an array of one content block of type "text". -func convertStringContentToArray(raw []byte) ([]byte, error) { - in := gjson.ParseBytes(raw) - - // Check if messages exist and need content conversion - if messages := in.Get("messages"); messages.Exists() { - var modifiedJSON map[string]interface{} - if err := json.Unmarshal(raw, &modifiedJSON); err != nil { - return raw, err - } - - convertStringContentRecursive(modifiedJSON) - - // Marshal back to JSON - return json.Marshal(modifiedJSON) - } - - return raw, nil -} - -// convertStringContentRecursive recursively scans JSON data and converts string "content" fields -// to proper text block arrays where needed for Anthropic SDK compatibility -func convertStringContentRecursive(data interface{}) { - switch v := data.(type) { - case map[string]interface{}: - // Check if this object has a "content" field with string value - if content, hasContent := v["content"]; hasContent { - if contentStr, isString := content.(string); isString { - // Check if this needs conversion based on context - if shouldConvertContentField(v) { - v["content"] = []map[string]interface{}{ - { - "type": "text", - "text": contentStr, - }, - } - } - } - } - - // Recursively process all values in the map - for _, value := range v { - convertStringContentRecursive(value) - } - - case []interface{}: - // Recursively process all items in the array - for _, item := range v { - convertStringContentRecursive(item) - } - } -} - -// shouldConvertContentField determines if a "content" string field should be converted to text block array -func shouldConvertContentField(obj map[string]interface{}) bool { - // Check if this is a message-level content (has "role" field) - if _, hasRole := obj["role"]; hasRole { - return true - } - - // Check if this is a tool_result block (but not mcp_tool_result which supports strings) - if objType, hasType := obj["type"].(string); hasType { - switch objType { - case "tool_result": - return true // Regular tool_result needs array format - case "mcp_tool_result": - return false // MCP tool_result supports strings - } - } - - return false -} - -// extractStreamFlag extracts the stream flag from JSON -func extractStreamFlag(raw []byte) bool { - in := gjson.ParseBytes(raw) - if streamVal := in.Get("stream"); streamVal.Exists() { - return streamVal.Bool() - } - return false -} - -// MessageNewParamsWrapper exists because the "stream" param is not included in anthropic.MessageNewParams. -type MessageNewParamsWrapper struct { - anthropic.MessageNewParams `json:""` - Stream bool `json:"stream,omitempty"` -} - -func (b MessageNewParamsWrapper) MarshalJSON() ([]byte, error) { - type shadow MessageNewParamsWrapper - return ant_param.MarshalWithExtras(b, (*shadow)(&b), map[string]any{ - "stream": b.Stream, - }) -} - -func (b *MessageNewParamsWrapper) UnmarshalJSON(raw []byte) error { - convertedRaw, err := convertStringContentToArray(raw) - if err != nil { - return err - } - - err = b.MessageNewParams.UnmarshalJSON(convertedRaw) - if err != nil { - return err - } - - b.Stream = extractStreamFlag(raw) - return nil -} - -func (b *MessageNewParamsWrapper) UseStreaming() bool { - return b.Stream -} - -// BetaMessageNewParamsWrapper exists because the "stream" param is not included in anthropic.BetaMessageNewParams. -type BetaMessageNewParamsWrapper struct { - anthropic.BetaMessageNewParams `json:""` - Stream bool `json:"stream,omitempty"` -} - -func (b BetaMessageNewParamsWrapper) MarshalJSON() ([]byte, error) { - type shadow BetaMessageNewParamsWrapper - return ant_param.MarshalWithExtras(b, (*shadow)(&b), map[string]any{ - "stream": b.Stream, - }) -} - -func (b *BetaMessageNewParamsWrapper) UnmarshalJSON(raw []byte) error { - convertedRaw, err := convertStringContentToArray(raw) - if err != nil { - return err - } - - err = b.BetaMessageNewParams.UnmarshalJSON(convertedRaw) - if err != nil { - return err - } - - b.Stream = extractStreamFlag(raw) - return nil -} - -func (b *BetaMessageNewParamsWrapper) UseStreaming() bool { - return b.Stream -} - -func (b *BetaMessageNewParamsWrapper) LastUserPrompt() (*string, error) { - if b == nil { - return nil, xerrors.New("nil struct") - } - - if len(b.Messages) == 0 { - return nil, xerrors.New("no messages") - } - - var userMessage string - for i := len(b.Messages) - 1; i >= 0; i-- { - m := b.Messages[i] - if m.Role != anthropic.BetaMessageParamRoleUser { - continue - } - if len(m.Content) == 0 { - continue - } - - for j := len(m.Content) - 1; j >= 0; j-- { - if textContent := m.Content[j].GetText(); textContent != nil { - userMessage = *textContent - } - - // Ignore internal Claude Code prompts. - if userMessage == "test" || - strings.Contains(userMessage, "") { - userMessage = "" - continue - } - - // Handle Cursor-specific formatting by extracting content from tags - if isCursor, _ := regexp.MatchString("", userMessage); isCursor { - userMessage = extractCursorUserQuery(userMessage) - } - return ptr.To(strings.TrimSpace(userMessage)), nil - } - } - - return nil, nil -} - -func extractCursorUserQuery(message string) string { - pat := regexp.MustCompile(`(?P[\s\S]*?)`) - match := pat.FindStringSubmatch(message) - if match != nil { - // Get the named group by index - contentIndex := pat.SubexpIndex("content") - if contentIndex != -1 { - message = match[contentIndex] - } - } - return strings.TrimSpace(message) -} diff --git a/aibridged/anthropic_utils.go b/aibridged/anthropic_utils.go deleted file mode 100644 index 06f40e433570f..0000000000000 --- a/aibridged/anthropic_utils.go +++ /dev/null @@ -1,45 +0,0 @@ -package aibridged - -import ( - "encoding/json" - "errors" - - "github.com/anthropics/anthropic-sdk-go" - ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" -) - -func getAnthropicErrorResponse(err error) *AnthropicErrorResponse { - var apierr *anthropic.Error - if !errors.As(err, &apierr) { - return nil - } - - msg := apierr.Error() - typ := string(ant_constant.ValueOf[ant_constant.APIError]()) - - var detail *anthropic.BetaAPIError - if field, ok := apierr.JSON.ExtraFields["error"]; ok { - _ = json.Unmarshal([]byte(field.Raw()), &detail) - } - if detail != nil { - msg = detail.Message - typ = string(detail.Type) - } - - return &AnthropicErrorResponse{ - BetaErrorResponse: &anthropic.BetaErrorResponse{ - Error: anthropic.BetaErrorUnion{ - Message: msg, - Type: typ, - }, - Type: ant_constant.ValueOf[ant_constant.Error](), - }, - StatusCode: apierr.StatusCode, - } -} - -type AnthropicErrorResponse struct { - *anthropic.BetaErrorResponse - - StatusCode int `json:"-"` -} diff --git a/aibridged/bridge.go b/aibridged/bridge.go deleted file mode 100644 index 5faf52caf9597..0000000000000 --- a/aibridged/bridge.go +++ /dev/null @@ -1,98 +0,0 @@ -package aibridged - -import ( - "fmt" - "net/http" - "time" - - "golang.org/x/xerrors" - - "cdr.dev/slog" - - "github.com/coder/coder/v2/aibridged/proto" -) - -const ( - ProviderOpenAI = "openai" - ProviderAnthropic = "anthropic" -) - -var providerRoutes = map[string][]string{ - ProviderOpenAI: {"/openai/v1/chat/completions"}, - ProviderAnthropic: {"/anthropic/v1/messages"}, -} - -// Bridge is responsible for proxying requests to upstream AI providers. -// -// Characteristics: -// 1. Client-side cancel -// 2. No timeout (SSE) -// 3a. client<->coderd conn established -// 3b. coderd<-> provider conn established -// 4a. requests from client<->coderd must be parsed, augmented, and relayed -// 4b. responses from provider->coderd must be parsed, optionally reflected back to client -// 5. tool calls may be injected and intercepted, transparently to the client -// 6. multiple calls can be made to provider while holding client<->coderd conn open -// 7. client<->coderd conn must ONLY be closed on client-side disconn or coderd<->provider non-recoverable error. -type Bridge struct { - httpSrv *http.Server - clientFn func() (proto.DRPCAIBridgeDaemonClient, error) - logger slog.Logger - - tools map[string]*MCPTool -} - -func NewBridge(registry ProviderRegistry, logger slog.Logger, clientFn func() (proto.DRPCAIBridgeDaemonClient, error), tools ToolRegistry) (*Bridge, error) { - drpcClient, err := clientFn() - if err != nil { - return nil, xerrors.Errorf("could not acquire coderd client for tracking: %w", err) - } - - mux := &http.ServeMux{} - for ident, provider := range registry { - routes, ok := providerRoutes[ident] - if !ok { - // Unknown provider identifier; skip. - continue - } - // Add the known provider-specific routes. - for _, path := range routes { - mux.HandleFunc(path, NewSessionProcessor(provider, logger, drpcClient, tools)) - } - - // Implement a catch-all route: any requests which fall through to this will be reverse-proxied to the upstream. - // Note: net/http ServeMux uses subtree matching when the pattern ends with a trailing slash. - fallthroughRoute := fmt.Sprintf("/%s/", ident) - mux.Handle(fallthroughRoute, http.StripPrefix(fallthroughRoute, - newFallthroughRouter(provider, logger.Named(fmt.Sprintf("%s.fallthrough", ident)))), - ) - } - - srv := &http.Server{ - Handler: mux, - - // TODO: configurable. - ReadTimeout: 30 * time.Second, - WriteTimeout: 0, // No write timeout for streaming responses. - IdleTimeout: 120 * time.Second, - ReadHeaderTimeout: 10 * time.Second, - } - - var bridge Bridge - bridge.httpSrv = srv - bridge.clientFn = clientFn - bridge.logger = logger - - bridge.tools = make(map[string]*MCPTool, len(tools)) - for _, serverTools := range tools { - for _, tool := range serverTools { - bridge.tools[tool.ID] = tool - } - } - - return &bridge, nil -} - -func (b *Bridge) Handler() http.Handler { - return b.httpSrv.Handler -} diff --git a/aibridged/bridge_integration_test.go b/aibridged/bridge_integration_test.go deleted file mode 100644 index 31bea35a6f3c3..0000000000000 --- a/aibridged/bridge_integration_test.go +++ /dev/null @@ -1,1014 +0,0 @@ -package aibridged_test - -import ( - "bufio" - "bytes" - "context" - _ "embed" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "net/http/httptest" - "sync" - "sync/atomic" - "testing" - - "golang.org/x/tools/txtar" - "golang.org/x/xerrors" - "storj.io/drpc" - - "github.com/anthropics/anthropic-sdk-go" - "github.com/anthropics/anthropic-sdk-go/packages/ssestream" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/sjson" - - "github.com/openai/openai-go" - oai_ssestream "github.com/openai/openai-go/packages/ssestream" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - - "github.com/coder/coder/v2/aibridged" - "github.com/coder/coder/v2/aibridged/proto" - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/drpcsdk" - "github.com/coder/coder/v2/testutil" -) - -var ( - //go:embed fixtures/anthropic/single_builtin_tool.txtar - antSingleBuiltinTool []byte - //go:embed fixtures/anthropic/single_injected_tool.txtar - antSingleInjectedTool []byte - - //go:embed fixtures/openai/single_builtin_tool.txtar - oaiSingleBuiltinTool []byte - //go:embed fixtures/openai/single_injected_tool.txtar - oaiSingleInjectedTool []byte - - //go:embed fixtures/anthropic/simple.txtar - antSimple []byte - - //go:embed fixtures/openai/simple.txtar - oaiSimple []byte - - //go:embed fixtures/anthropic/fallthrough.txtar - antFallthrough []byte - - //go:embed fixtures/openai/fallthrough.txtar - oaiFallthrough []byte -) - -const ( - fixtureRequest = "request" - fixtureStreamingResponse = "streaming" - fixtureNonStreamingResponse = "non-streaming" - fixtureStreamingToolResponse = "streaming/tool-call" - fixtureNonStreamingToolResponse = "non-streaming/tool-call" - fixtureResponse = "response" -) - -func TestAnthropicMessages(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, nil) - sessionToken := getSessionToken(t, client) - - t.Run("single builtin tool", func(t *testing.T) { - t.Parallel() - - cases := []struct { - streaming bool - expectedInputTokens, expectedOutputTokens int - }{ - { - streaming: true, - expectedInputTokens: 2, - expectedOutputTokens: 66, - }, - { - streaming: false, - expectedInputTokens: 5, - expectedOutputTokens: 84, - }, - } - - for _, tc := range cases { - t.Run(fmt.Sprintf("%s/streaming=%v", t.Name(), tc.streaming), func(t *testing.T) { - t.Parallel() - - arc := txtar.Parse(antSingleBuiltinTool) - t.Logf("%s: %s", t.Name(), arc.Comment) - - files := filesMap(arc) - require.Len(t, files, 3) - require.Contains(t, files, fixtureRequest) - require.Contains(t, files, fixtureStreamingResponse) - require.Contains(t, files, fixtureNonStreamingResponse) - - reqBody := files[fixtureRequest] - - // Add the stream param to the request. - newBody, err := sjson.SetBytes(reqBody, "stream", tc.streaming) - require.NoError(t, err) - reqBody = newBody - - ctx := testutil.Context(t, testutil.WaitLong) - srv := newMockServer(ctx, t, files, nil) - t.Cleanup(srv.Close) - - coderdClient := &fakeBridgeDaemonClient{} - - logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) - registry := aibridged.ProviderRegistry{ - aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(srv.URL, sessionToken), - } - b, err := aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { - return coderdClient, nil - }, nil) - require.NoError(t, err) - - mockSrv := httptest.NewServer(withInitiator(getCurrentUserID(t, client), b.Handler())) - // Make API call to aibridge for Anthropic /v1/messages - req := createAnthropicMessagesReq(t, mockSrv.URL, reqBody) - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - defer resp.Body.Close() - - // Response-specific checks. - if tc.streaming { - sp := aibridged.NewSSEParser() - require.NoError(t, sp.Parse(resp.Body)) - - // Ensure the message starts and completes, at a minimum. - assert.Contains(t, sp.AllEvents(), "message_start") - assert.Contains(t, sp.AllEvents(), "message_stop") - } - - require.Len(t, coderdClient.tokenUsages, 1) - - assert.EqualValues(t, tc.expectedInputTokens, calculateTotalInputTokens(coderdClient.tokenUsages), "input tokens miscalculated") - assert.EqualValues(t, tc.expectedOutputTokens, calculateTotalOutputTokens(coderdClient.tokenUsages), "output tokens miscalculated") - - var args map[string]any - require.NoError(t, json.Unmarshal([]byte(coderdClient.toolUsages[0].Input), &args)) - - require.Len(t, coderdClient.toolUsages, 1) - assert.Equal(t, "Read", coderdClient.toolUsages[0].Tool) - require.Contains(t, args, "file_path") - assert.Equal(t, "/tmp/blah/foo", args["file_path"]) - - require.Len(t, coderdClient.userPrompts, 1) - assert.Equal(t, "read the foo file", coderdClient.userPrompts[0].Prompt) - }) - } - }) -} - -func TestOpenAIChatCompletions(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, nil) - sessionToken := getSessionToken(t, client) - - t.Run("single builtin tool", func(t *testing.T) { - t.Parallel() - - cases := []struct { - streaming bool - expectedInputTokens, expectedOutputTokens int - }{ - { - streaming: true, - expectedInputTokens: 60, - expectedOutputTokens: 15, - }, - { - streaming: false, - expectedInputTokens: 60, - expectedOutputTokens: 15, - }, - } - - for _, tc := range cases { - t.Run(fmt.Sprintf("%s/streaming=%v", t.Name(), tc.streaming), func(t *testing.T) { - t.Parallel() - - arc := txtar.Parse(oaiSingleBuiltinTool) - t.Logf("%s: %s", t.Name(), arc.Comment) - - files := filesMap(arc) - require.Len(t, files, 3) - require.Contains(t, files, fixtureRequest) - require.Contains(t, files, fixtureStreamingResponse) - require.Contains(t, files, fixtureNonStreamingResponse) - - reqBody := files[fixtureRequest] - - // Add the stream param to the request. - newBody, err := sjson.SetBytes(reqBody, "stream", tc.streaming) - require.NoError(t, err) - reqBody = newBody - - ctx := testutil.Context(t, testutil.WaitLong) - srv := newMockServer(ctx, t, files, nil) - t.Cleanup(srv.Close) - - coderdClient := &fakeBridgeDaemonClient{} - - logger := testutil.Logger(t) // slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) - registry := aibridged.ProviderRegistry{ - aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(srv.URL, sessionToken), - } - b, err := aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { - return coderdClient, nil - }, nil) - require.NoError(t, err) - - mockSrv := httptest.NewServer(withInitiator(getCurrentUserID(t, client), b.Handler())) - // Make API call to aibridge for OpenAI /v1/chat/completions - req := createOpenAIChatCompletionsReq(t, mockSrv.URL, reqBody) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - defer resp.Body.Close() - - // Response-specific checks. - if tc.streaming { - sp := aibridged.NewSSEParser() - require.NoError(t, sp.Parse(resp.Body)) - - // OpenAI sends all events under the same type. - messageEvents := sp.MessageEvents() - assert.NotEmpty(t, messageEvents) - - // OpenAI streaming ends with [DONE] - lastEvent := messageEvents[len(messageEvents)-1] - assert.Equal(t, "[DONE]", lastEvent.Data) - } - - require.Len(t, coderdClient.tokenUsages, 1) - assert.EqualValues(t, tc.expectedInputTokens, calculateTotalInputTokens(coderdClient.tokenUsages), "input tokens miscalculated") - assert.EqualValues(t, tc.expectedOutputTokens, calculateTotalOutputTokens(coderdClient.tokenUsages), "output tokens miscalculated") - - var args map[string]any - require.NoError(t, json.Unmarshal([]byte(coderdClient.toolUsages[0].Input), &args)) - - require.Len(t, coderdClient.toolUsages, 1) - assert.Equal(t, "read_file", coderdClient.toolUsages[0].Tool) - require.Contains(t, args, "path") - assert.Equal(t, "README.md", args["path"]) - - require.Len(t, coderdClient.userPrompts, 1) - assert.Equal(t, "how large is the README.md file in my current path", coderdClient.userPrompts[0].Prompt) - }) - } - }) -} - -func TestSimple(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, nil) - sessionToken := getSessionToken(t, client) - - testCases := []struct { - name string - fixture []byte - configureFunc func(string, proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) - getResponseIDFunc func(bool, *http.Response) (string, error) - createRequest func(*testing.T, string, []byte) *http.Request - }{ - { - name: aibridged.ProviderAnthropic, - fixture: antSimple, - configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { - logger := testutil.Logger(t) - registry := aibridged.ProviderRegistry{ - aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(addr, sessionToken), - } - return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { - return client, nil - }, nil) - }, - getResponseIDFunc: func(streaming bool, resp *http.Response) (string, error) { - if streaming { - decoder := ssestream.NewDecoder(resp) - // TODO: this is a bit flimsy since this API won't be in beta forever. - stream := ssestream.NewStream[anthropic.BetaRawMessageStreamEventUnion](decoder, nil) - var message anthropic.BetaMessage - for stream.Next() { - event := stream.Current() - if err := message.Accumulate(event); err != nil { - return "", xerrors.Errorf("accumulate event: %w", err) - } - } - if stream.Err() != nil { - return "", xerrors.Errorf("stream error: %w", stream.Err()) - } - return message.ID, nil - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", xerrors.Errorf("read body: %w", err) - } - - // TODO: this is a bit flimsy since this API won't be in beta forever. - var message anthropic.BetaMessage - if err := json.Unmarshal(body, &message); err != nil { - return "", xerrors.Errorf("unmarshal response: %w", err) - } - return message.ID, nil - }, - createRequest: createAnthropicMessagesReq, - }, - { - name: aibridged.ProviderOpenAI, - fixture: oaiSimple, - configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { - logger := testutil.Logger(t) - registry := aibridged.ProviderRegistry{ - aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(addr, sessionToken), - } - return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { - return client, nil - }, nil) - }, - getResponseIDFunc: func(streaming bool, resp *http.Response) (string, error) { - if streaming { - // Parse the response stream. - decoder := oai_ssestream.NewDecoder(resp) - stream := oai_ssestream.NewStream[openai.ChatCompletionChunk](decoder, nil) - var message openai.ChatCompletionAccumulator - for stream.Next() { - chunk := stream.Current() - message.AddChunk(chunk) - } - if stream.Err() != nil { - return "", xerrors.Errorf("stream error: %w", stream.Err()) - } - return message.ID, nil - } - - // Parse & unmarshal the response. - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", xerrors.Errorf("read body: %w", err) - } - - var message openai.ChatCompletion - if err := json.Unmarshal(body, &message); err != nil { - return "", xerrors.Errorf("unmarshal response: %w", err) - } - return message.ID, nil - }, - createRequest: createOpenAIChatCompletionsReq, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - streamingCases := []struct { - streaming bool - }{ - {streaming: true}, - {streaming: false}, - } - - for _, sc := range streamingCases { - t.Run(fmt.Sprintf("streaming=%v", sc.streaming), func(t *testing.T) { - t.Parallel() - - arc := txtar.Parse(tc.fixture) - t.Logf("%s: %s", t.Name(), arc.Comment) - - files := filesMap(arc) - require.Len(t, files, 3) - require.Contains(t, files, fixtureRequest) - require.Contains(t, files, fixtureStreamingResponse) - require.Contains(t, files, fixtureNonStreamingResponse) - - reqBody := files[fixtureRequest] - - // Add the stream param to the request. - newBody, err := sjson.SetBytes(reqBody, "stream", sc.streaming) - require.NoError(t, err) - reqBody = newBody - - // Given: a mock API server and a Bridge through which the requests will flow. - ctx := testutil.Context(t, testutil.WaitLong) - srv := newMockServer(ctx, t, files, nil) - t.Cleanup(srv.Close) - - coderdClient := &fakeBridgeDaemonClient{} - - b, err := tc.configureFunc(srv.URL, coderdClient) - require.NoError(t, err) - - mockSrv := httptest.NewServer(withInitiator(getCurrentUserID(t, client), b.Handler())) - // When: calling the "API server" with the fixture's request body. - req := tc.createRequest(t, mockSrv.URL, reqBody) - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - defer resp.Body.Close() - - // Then: I expect a non-empty response. - bodyBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - assert.NotEmpty(t, bodyBytes, "should have received response body") - - // Reset the body after being read. - resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - - // Then: I expect the prompt to have been tracked. - require.NotEmpty(t, coderdClient.userPrompts, "no prompts tracked") - assert.Equal(t, "how many angels can dance on the head of a pin", coderdClient.userPrompts[0].Prompt) - - // Validate that responses have their IDs overridden with a session ID rather than the original ID from the upstream provider. - // The reason for this is that Bridge may make multiple upstream requests (i.e. to invoke injected tools), and clients will not be expecting - // multiple messages in response to a single request. - // TODO: validate that expected upstream message ID is captured alongside returned ID in token usage. - id, err := tc.getResponseIDFunc(sc.streaming, resp) - require.NoError(t, err, "failed to retrieve response ID") - require.Nil(t, uuid.Validate(id), "id is not a UUID") - }) - } - }) - } -} - -func TestFallthrough(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, nil) - sessionToken := getSessionToken(t, client) - - testCases := []struct { - provider string - fixture []byte - authHeaderName string - configureFunc func(string, proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) - }{ - { - provider: aibridged.ProviderAnthropic, - fixture: antFallthrough, - configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { - logger := testutil.Logger(t) - registry := aibridged.ProviderRegistry{ - aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(addr, sessionToken), - } - return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { - return client, nil - }, nil) - }, - }, - { - provider: aibridged.ProviderOpenAI, - fixture: oaiFallthrough, - configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient) (*aibridged.Bridge, error) { - logger := testutil.Logger(t) - registry := aibridged.ProviderRegistry{ - aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(addr, sessionToken), - } - return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { - return client, nil - }, nil) - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.provider, func(t *testing.T) { - t.Parallel() - - arc := txtar.Parse(tc.fixture) - t.Logf("%s: %s", t.Name(), arc.Comment) - - files := filesMap(arc) - require.Contains(t, files, fixtureResponse) - - respBody := files[fixtureResponse] - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/v1/models" { - t.Errorf("unexpected request path: %q", r.URL.Path) - t.FailNow() - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(respBody) - })) - t.Cleanup(upstream.Close) - - coderdClient := &fakeBridgeDaemonClient{} - - bridge, err := tc.configureFunc(upstream.URL, coderdClient) - require.NoError(t, err) - - bridgeSrv := httptest.NewServer(bridge.Handler()) - t.Cleanup(bridgeSrv.Close) - - req, err := http.NewRequestWithContext(t.Context(), "GET", fmt.Sprintf("%s/%s/v1/models", bridgeSrv.URL, tc.provider), nil) - require.NoError(t, err) - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - require.Equal(t, http.StatusOK, resp.StatusCode) - - gotBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - // Compare JSON bodies for semantic equality. - var got any - var exp any - require.NoError(t, json.Unmarshal(gotBytes, &got)) - require.NoError(t, json.Unmarshal(respBody, &exp)) - require.EqualValues(t, exp, got) - }) - } -} - -// setupMCPToolsForTest creates a mock MCP server, initializes the MCP bridge, and returns the tools -func setupMCPToolsForTest(t *testing.T) map[string][]*aibridged.MCPTool { - t.Helper() - - // Setup Coder MCP integration - mcpSrv := httptest.NewServer(createMockMCPSrv(t)) - t.Cleanup(mcpSrv.Close) - - logger := testutil.Logger(t) - mcpBridge, err := aibridged.NewMCPToolBridge("coder", mcpSrv.URL, map[string]string{}, logger) - require.NoError(t, err) - - // Initialize MCP client, fetch tools, and inject into bridge - require.NoError(t, mcpBridge.Init(testutil.Context(t, testutil.WaitShort))) - tools := mcpBridge.ListTools() - require.NotEmpty(t, tools) - - return map[string][]*aibridged.MCPTool{ - "coder": tools, - } -} - -// TestInjectedTool is an abstracted test function for "single injected tool" scenarios -// that works with both Anthropic and OpenAI providers -func TestInjectedTool(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, nil) - sessionToken := getSessionToken(t, client) - - testCases := []struct { - name string - fixture []byte - configureFunc func(string, proto.DRPCAIBridgeDaemonClient, map[string][]*aibridged.MCPTool) (*aibridged.Bridge, error) - getResponseContentFunc func(bool, *http.Response) (string, error) - createRequest func(*testing.T, string, []byte) *http.Request - }{ - { - name: aibridged.ProviderAnthropic, - fixture: antSingleInjectedTool, - configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools map[string][]*aibridged.MCPTool) (*aibridged.Bridge, error) { - logger := testutil.Logger(t) - registry := aibridged.ProviderRegistry{ - aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(addr, sessionToken), - } - return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { - return client, nil - }, tools) - }, - getResponseContentFunc: func(streaming bool, resp *http.Response) (string, error) { - // TODO: this is a bit flimsy since this API won't be in beta forever. - var content *anthropic.BetaContentBlockUnion - if streaming { - // Parse the response stream. - decoder := ssestream.NewDecoder(resp) - stream := ssestream.NewStream[anthropic.BetaRawMessageStreamEventUnion](decoder, nil) - var message anthropic.BetaMessage - for stream.Next() { - event := stream.Current() - if err := message.Accumulate(event); err != nil { - return "", xerrors.Errorf("accumulate event: %w", err) - } - } - if stream.Err() != nil { - return "", xerrors.Errorf("stream error: %w", stream.Err()) - } - if len(message.Content) < 2 { - return "", xerrors.Errorf("expected at least 2 content blocks, got %d", len(message.Content)) - } - content = &message.Content[1] - } else { - // Parse & unmarshal the response. - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", xerrors.Errorf("read body: %w", err) - } - - var message anthropic.BetaMessage - if err := json.Unmarshal(body, &message); err != nil { - return "", xerrors.Errorf("unmarshal response: %w", err) - } - if len(message.Content) == 0 { - return "", xerrors.Errorf("no content blocks in response") - } - content = &message.Content[0] - } - - if content == nil { - return "", xerrors.Errorf("content is nil") - } - return content.Text, nil - }, - createRequest: createAnthropicMessagesReq, - }, - { - name: aibridged.ProviderOpenAI, - fixture: oaiSingleInjectedTool, - configureFunc: func(addr string, client proto.DRPCAIBridgeDaemonClient, tools map[string][]*aibridged.MCPTool) (*aibridged.Bridge, error) { - logger := testutil.Logger(t) - registry := aibridged.ProviderRegistry{ - aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(addr, sessionToken), - } - return aibridged.NewBridge(registry, logger, func() (proto.DRPCAIBridgeDaemonClient, error) { - return client, nil - }, tools) - }, - getResponseContentFunc: func(streaming bool, resp *http.Response) (string, error) { - var content *openai.ChatCompletionChoice - if streaming { - // Parse the response stream. - decoder := oai_ssestream.NewDecoder(resp) - stream := oai_ssestream.NewStream[openai.ChatCompletionChunk](decoder, nil) - var message openai.ChatCompletionAccumulator - for stream.Next() { - chunk := stream.Current() - message.AddChunk(chunk) - } - - if stream.Err() != nil { - return "", xerrors.Errorf("stream error: %w", stream.Err()) - } - if len(message.Choices) == 0 { - return "", xerrors.Errorf("no choices in response") - } - content = &message.Choices[0] - } else { - // Parse & unmarshal the response. - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", xerrors.Errorf("read body: %w", err) - } - - var message openai.ChatCompletion - if err := json.Unmarshal(body, &message); err != nil { - return "", xerrors.Errorf("unmarshal response: %w", err) - } - if len(message.Choices) == 0 { - return "", xerrors.Errorf("no choices in response") - } - content = &message.Choices[0] - } - - if content == nil { - return "", xerrors.Errorf("content is nil") - } - return content.Message.Content, nil - }, - createRequest: createOpenAIChatCompletionsReq, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - streamingCases := []struct { - streaming bool - }{ - {streaming: true}, - {streaming: false}, - } - - for _, sc := range streamingCases { - t.Run(fmt.Sprintf("streaming=%v", sc.streaming), func(t *testing.T) { - t.Parallel() - - arc := txtar.Parse(tc.fixture) - t.Logf("%s: %s", t.Name(), arc.Comment) - - files := filesMap(arc) - require.Len(t, files, 5) - require.Contains(t, files, fixtureRequest) - require.Contains(t, files, fixtureStreamingResponse) - require.Contains(t, files, fixtureNonStreamingResponse) - require.Contains(t, files, fixtureStreamingToolResponse) - require.Contains(t, files, fixtureNonStreamingToolResponse) - - reqBody := files[fixtureRequest] - - // Add the stream param to the request. - newBody, err := sjson.SetBytes(reqBody, "stream", sc.streaming) - require.NoError(t, err) - reqBody = newBody - - ctx := testutil.Context(t, testutil.WaitLong) - - // Setup mock server with response mutator for multi-turn interaction. - mockSrv := newMockServer(ctx, t, files, func(reqCount uint32, resp []byte) []byte { - if reqCount == 1 { - return resp // First request gets the normal response (with tool call) - } - - if reqCount > 2 { - // This should not happen in single injected tool tests - return resp - } - - // Second request gets the tool response - if sc.streaming { - return files[fixtureStreamingToolResponse] - } - return files[fixtureNonStreamingToolResponse] - }) - t.Cleanup(mockSrv.Close) - - coderdClient := &fakeBridgeDaemonClient{} - - // Setup MCP tools. - tools := setupMCPToolsForTest(t) - - // Configure the bridge with injected tools. - b, err := tc.configureFunc(mockSrv.URL, coderdClient, tools) - require.NoError(t, err) - - // Invoke request to mocked API via aibridge. - bridgeSrv := httptest.NewServer(withInitiator(getCurrentUserID(t, client), b.Handler())) - t.Cleanup(bridgeSrv.Close) - - req := tc.createRequest(t, bridgeSrv.URL, reqBody) - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - defer resp.Body.Close() - - // We must ALWAYS have 2 calls to the bridge for injected tool tests - require.Eventually(t, func() bool { - return mockSrv.callCount.Load() == 2 - }, testutil.WaitLong, testutil.IntervalFast) - - // Ensure expected tool was invoked with expected input. - require.Len(t, coderdClient.toolUsages, 1) - require.Equal(t, mockToolName, coderdClient.toolUsages[0].Tool) - require.EqualValues(t, `{"owner":"admin"}`, coderdClient.toolUsages[0].Input) - - // Ensure tool returned expected value. - answer, err := tc.getResponseContentFunc(sc.streaming, resp) - require.NoError(t, err) - require.Contains(t, answer, "dd711d5c-83c6-4c08-a0af-b73055906e8c") // The ID of the workspace to be returned. - }) - } - }) - } -} - -func calculateTotalOutputTokens(in []*proto.TrackTokenUsageRequest) int64 { - var total int64 - for _, el := range in { - total += el.OutputTokens - } - return total -} - -func calculateTotalInputTokens(in []*proto.TrackTokenUsageRequest) int64 { - var total int64 - for _, el := range in { - total += el.InputTokens - } - return total -} - -type archiveFileMap map[string][]byte - -func filesMap(archive *txtar.Archive) archiveFileMap { - if len(archive.Files) == 0 { - return nil - } - - out := make(archiveFileMap, len(archive.Files)) - for _, f := range archive.Files { - out[f.Name] = f.Data - } - return out -} - -func createAnthropicMessagesReq(t *testing.T, baseURL string, input []byte) *http.Request { - t.Helper() - - req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/anthropic/v1/messages", bytes.NewReader(input)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - return req -} - -func createOpenAIChatCompletionsReq(t *testing.T, baseURL string, input []byte) *http.Request { - t.Helper() - - req, err := http.NewRequestWithContext(t.Context(), "POST", baseURL+"/openai/v1/chat/completions", bytes.NewReader(input)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - return req -} - -func getSessionToken(t *testing.T, client *codersdk.Client) string { - t.Helper() - - _ = coderdtest.CreateFirstUser(t, client) - resp, err := client.LoginWithPassword(t.Context(), codersdk.LoginWithPasswordRequest{ - Email: coderdtest.FirstUserParams.Email, - Password: coderdtest.FirstUserParams.Password, - }) - - require.NoError(t, err) - return resp.SessionToken -} - -type mockServer struct { - *httptest.Server - - callCount atomic.Uint32 -} - -func newMockServer(ctx context.Context, t *testing.T, files archiveFileMap, responseMutatorFn func(reqCount uint32, resp []byte) []byte) *mockServer { - t.Helper() - - ms := &mockServer{} - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ms.callCount.Add(1) - - body, err := io.ReadAll(r.Body) - defer r.Body.Close() - require.NoError(t, err) - - type msg struct { - Stream bool `json:"stream"` - } - var reqMsg msg - require.NoError(t, json.Unmarshal(body, &reqMsg)) - - if !reqMsg.Stream { - resp := files[fixtureNonStreamingResponse] - if responseMutatorFn != nil { - resp = responseMutatorFn(ms.callCount.Load(), resp) - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write(resp) - return - } - - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") - - resp := files[fixtureStreamingResponse] - if responseMutatorFn != nil { - resp = responseMutatorFn(ms.callCount.Load(), resp) - } - - scanner := bufio.NewScanner(bytes.NewReader(resp)) - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "Streaming unsupported", http.StatusInternalServerError) - return - } - - for scanner.Scan() { - line := scanner.Text() - - fmt.Fprintf(w, "%s\n", line) - flusher.Flush() - } - - if err := scanner.Err(); err != nil { - http.Error(w, fmt.Sprintf("Error reading fixture: %v", err), http.StatusInternalServerError) - return - } - })) - t.Cleanup(srv.Close) - - srv.Config.BaseContext = func(_ net.Listener) context.Context { - return ctx - } - - ms.Server = srv - return ms -} - -var _ proto.DRPCAIBridgeDaemonClient = &fakeBridgeDaemonClient{} - -type fakeBridgeDaemonClient struct { - mu sync.Mutex - - sessions []*proto.StartSessionRequest - tokenUsages []*proto.TrackTokenUsageRequest - userPrompts []*proto.TrackUserPromptRequest - toolUsages []*proto.TrackToolUsageRequest -} - -func (*fakeBridgeDaemonClient) DRPCConn() drpc.Conn { - conn, _ := drpcsdk.MemTransportPipe() - return conn -} - -// StartSession implements proto.DRPCAIBridgeDaemonClient. -func (f *fakeBridgeDaemonClient) StartSession(ctx context.Context, in *proto.StartSessionRequest) (*proto.StartSessionResponse, error) { - f.mu.Lock() - defer f.mu.Unlock() - f.sessions = append(f.sessions, in) - - return &proto.StartSessionResponse{}, nil -} - -func (f *fakeBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { - f.mu.Lock() - defer f.mu.Unlock() - f.tokenUsages = append(f.tokenUsages, in) - - return &proto.TrackTokenUsageResponse{}, nil -} - -func (f *fakeBridgeDaemonClient) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptRequest) (*proto.TrackUserPromptResponse, error) { - f.mu.Lock() - defer f.mu.Unlock() - f.userPrompts = append(f.userPrompts, in) - - return &proto.TrackUserPromptResponse{}, nil -} - -func (f *fakeBridgeDaemonClient) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageRequest) (*proto.TrackToolUsageResponse, error) { - f.mu.Lock() - defer f.mu.Unlock() - f.toolUsages = append(f.toolUsages, in) - - return &proto.TrackToolUsageResponse{}, nil -} - -const mockToolName = "coder_list_workspaces" - -func createMockMCPSrv(t *testing.T) http.Handler { - t.Helper() - - s := server.NewMCPServer( - "Mock coder MCP server", - "1.0.0", - server.WithToolCapabilities(true), - ) - - tool := mcp.NewTool(mockToolName, - mcp.WithDescription(fmt.Sprintf("Mock of the %s tool", mockToolName)), - ) - s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return mcp.NewToolResultText("mock"), nil - }) - - return server.NewStreamableHTTPServer(s) -} - -// withInitiator wraps a handler injecting the Bridge user ID into context. -// TODO: this is only necessary because we're not exercising the real API's middleware, which may hide some problems. -func withInitiator(userID uuid.UUID, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), aibridged.ContextKeyBridgeUserID{}, userID) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func getCurrentUserID(t *testing.T, client *codersdk.Client) uuid.UUID { - t.Helper() - - me, err := client.User(t.Context(), "me") - require.NoError(t, err) - return me.ID -} diff --git a/aibridged/fallthrough.go b/aibridged/fallthrough.go deleted file mode 100644 index c376ddb838d4a..0000000000000 --- a/aibridged/fallthrough.go +++ /dev/null @@ -1,104 +0,0 @@ -package aibridged - -import ( - "net" - "net/http" - "net/http/httputil" - "net/url" - "time" - - "cdr.dev/slog" -) - -// fallthroughRouter is a simple reverse-proxy implementation to enable -type fallthroughRouter struct { - provider Provider - logger slog.Logger -} - -func newFallthroughRouter(provider Provider, logger slog.Logger) *fallthroughRouter { - return &fallthroughRouter{ - provider: provider, - logger: logger, - } -} - -func (f *fallthroughRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { - upURL, err := url.Parse(f.provider.BaseURL()) - if err != nil { - f.logger.Error(r.Context(), "failed to parse provider base URL", slog.Error(err)) - http.Error(w, "request error", http.StatusBadGateway) - return - } - - // Build a reverse proxy to the upstream. - proxy := &httputil.ReverseProxy{ - Director: func(req *http.Request) { - // Set scheme/host to upstream. - req.URL.Scheme = upURL.Scheme - req.URL.Host = upURL.Host - - // Preserve the stripped path from the incoming request and ensure leading slash. - p := r.URL.Path - if len(p) == 0 || p[0] != '/' { - p = "/" + p - } - req.URL.Path = p - req.URL.RawPath = "" - - // Preserve query string. - req.URL.RawQuery = r.URL.RawQuery - - // Set Host header for upstream. - req.Host = upURL.Host - - // Copy headers from client. - req.Header = r.Header.Clone() - - // Standard proxy headers. - host, _, herr := net.SplitHostPort(r.RemoteAddr) - if herr != nil { - host = r.RemoteAddr - } - if prior := req.Header.Get("X-Forwarded-For"); prior != "" { - req.Header.Set("X-Forwarded-For", prior+", "+host) - } else { - req.Header.Set("X-Forwarded-For", host) - } - req.Header.Set("X-Forwarded-Host", r.Host) - if r.TLS != nil { - req.Header.Set("X-Forwarded-Proto", "https") - } else { - req.Header.Set("X-Forwarded-Proto", "http") - } - // Avoid default Go user-agent if none provided. - if _, ok := req.Header["User-Agent"]; !ok { - req.Header.Set("User-Agent", "aibridged") // TODO: use build tag. - } - - // Inject provider auth. - switch f.provider.(type) { - case *OpenAIProvider: - req.Header.Set("Authorization", "Bearer "+f.provider.Key()) - case *AnthropicMessagesProvider: - req.Header.Set("x-api-key", f.provider.Key()) - } - }, - ErrorHandler: func(rw http.ResponseWriter, req *http.Request, e error) { - f.logger.Error(req.Context(), "reverse proxy error", slog.Error(e), slog.F("path", req.URL.Path)) - http.Error(rw, "upstream proxy error", http.StatusBadGateway) - }, - } - - // Transport tuned for streaming (no response header timeout). - proxy.Transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - - proxy.ServeHTTP(w, r) -} diff --git a/aibridged/fixtures/README.md b/aibridged/fixtures/README.md deleted file mode 100644 index 075eaed0a3253..0000000000000 --- a/aibridged/fixtures/README.md +++ /dev/null @@ -1,25 +0,0 @@ -These fixtures were created by adding logging middleware to API calls to view the raw requests/responses. - -```go -... -opts = append(opts, option.WithMiddleware(LoggingMiddleware)) -... - -func LoggingMiddleware(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { - reqOut, _ := httputil.DumpRequest(req, true) - - // Forward the request to the next handler - res, err = next(req) - fmt.Printf("[req] %s\n", reqOut) - - // Handle stuff after the request - if err != nil { - return res, err - } - - respOut, _ := httputil.DumpResponse(res, true) - fmt.Printf("[resp] %s\n", respOut) - - return res, err -} -``` diff --git a/aibridged/fixtures/anthropic/fallthrough.txtar b/aibridged/fixtures/anthropic/fallthrough.txtar deleted file mode 100644 index 6d9801d9620c5..0000000000000 --- a/aibridged/fixtures/anthropic/fallthrough.txtar +++ /dev/null @@ -1,64 +0,0 @@ -API endpoints not explicitly handled will fallthrough to upstream via reverse-proxy. - --- response -- -{ - "data": [ - { - "type": "model", - "id": "claude-opus-4-1-20250805", - "display_name": "Claude Opus 4.1", - "created_at": "2025-08-05T00:00:00Z" - }, - { - "type": "model", - "id": "claude-opus-4-20250514", - "display_name": "Claude Opus 4", - "created_at": "2025-05-22T00:00:00Z" - }, - { - "type": "model", - "id": "claude-sonnet-4-20250514", - "display_name": "Claude Sonnet 4", - "created_at": "2025-05-22T00:00:00Z" - }, - { - "type": "model", - "id": "claude-3-7-sonnet-20250219", - "display_name": "Claude Sonnet 3.7", - "created_at": "2025-02-24T00:00:00Z" - }, - { - "type": "model", - "id": "claude-3-5-sonnet-20241022", - "display_name": "Claude Sonnet 3.5 (New)", - "created_at": "2024-10-22T00:00:00Z" - }, - { - "type": "model", - "id": "claude-3-5-haiku-20241022", - "display_name": "Claude Haiku 3.5", - "created_at": "2024-10-22T00:00:00Z" - }, - { - "type": "model", - "id": "claude-3-5-sonnet-20240620", - "display_name": "Claude Sonnet 3.5 (Old)", - "created_at": "2024-06-20T00:00:00Z" - }, - { - "type": "model", - "id": "claude-3-haiku-20240307", - "display_name": "Claude Haiku 3", - "created_at": "2024-03-07T00:00:00Z" - }, - { - "type": "model", - "id": "claude-3-opus-20240229", - "display_name": "Claude Opus 3", - "created_at": "2024-02-29T00:00:00Z" - } - ], - "has_more": false, - "first_id": "claude-opus-4-1-20250805", - "last_id": "claude-3-opus-20240229" -} diff --git a/aibridged/fixtures/anthropic/simple.txtar b/aibridged/fixtures/anthropic/simple.txtar deleted file mode 100644 index 9cf6084fe40d2..0000000000000 --- a/aibridged/fixtures/anthropic/simple.txtar +++ /dev/null @@ -1,139 +0,0 @@ -Simple request. - --- request -- -{ - "max_tokens": 8192, - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "how many angels can dance on the head of a pin\n" - } - ] - } - ], - "model": "claude-sonnet-4-0", - "temperature": 1 -} - --- streaming -- -event: message_start -data: {"type":"message_start","message":{"id":"msg_01FeMXMphebFfFsTWMNHex7P","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} } - -event: content_block_start -data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } - -event: ping -data: {"type": "ping"} - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"This"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" is a famous philosophical question often used to illustrate medieval"}} - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" scholastic debates that seem pointless or ov"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"erly abstract. The question \"How many angels can dance on the head of"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" a pin?\" is typically cited as an example of us"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"eless speculation.\n\nHistorically, medieval theolog"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ians did debate the nature of angels -"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" whether they were incorporeal beings, how"}} - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" they occupied space, and whether multiple angels could exist"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" in the same location. However, there"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'s little evidence they literally"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" debated dancing angels on pinheads.\n\nThe question has"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" no factual answer since it depends on assumptions about:"}} - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n- The existence and nature of angels\n- Whether"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" incorporeal beings occupy physical space\n- What"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" constitutes \"dancing\" for a spiritual"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" entity\n- The size of both the"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" pin and the angels\n\nIt's become a metaph"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"or for overthinking trivial matters"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" or getting lost in theoretical discussions disconnected from practical reality."} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Some use it to critique certain types of academic"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" or theological debate, while others defen"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d the value of exploring fundamental questions about existence an"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d metaphysics.\n\nSo while u"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"nanswerable literally, it serves as an interesting lens"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for discussing the nature of philosophical inquiry itself."} } - -event: content_block_stop -data: {"type":"content_block_stop","index":0 } - -event: message_delta -data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":240} } - -event: message_stop -data: {"type":"message_stop" } - --- non-streaming -- -{ - "id": "msg_01Pvyf26bY17RcjmWfJsXGBn", - "type": "message", - "role": "assistant", - "model": "claude-sonnet-4-20250514", - "content": [ - { - "type": "text", - "text": "This is a famous philosophical question, often called \"How many angels can dance on the head of a pin?\" It's typically used to represent pointless or overly abstract theological debates.\n\nThe question doesn't have a literal answer because:\n\n1. **Historical context**: It's often attributed to medieval scholastic philosophers, though there's little evidence they actually debated this exact question. It became a popular way to mock what some saw as useless academic arguments.\n\n2. **Philosophical purpose**: The question highlights the difficulty of discussing non-physical beings (angels) in physical terms (space on a pinhead).\n\n3. **Different interpretations**: \n - If angels are purely spiritual, they might not take up physical space at all\n - If they do occupy space, we'd need to know their \"size\"\n - The question might be asking about the nature of space, matter, and spirit\n\nSo the real answer is that it's not meant to be answered literally - it's a thought experiment about the limits of rational inquiry and the sometimes absurd directions theological speculation can take.\n\nWould you like to explore the philosophical implications behind this question, or were you thinking about it in a different context?" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 18, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - "output_tokens": 254, - "service_tier": "standard" - } -} diff --git a/aibridged/fixtures/anthropic/single_builtin_tool.txtar b/aibridged/fixtures/anthropic/single_builtin_tool.txtar deleted file mode 100644 index e095649cae18f..0000000000000 --- a/aibridged/fixtures/anthropic/single_builtin_tool.txtar +++ /dev/null @@ -1,127 +0,0 @@ -Claude Code has builtin tools to (e.g.) explore the filesystem. - --- request -- -{ - "model": "claude-sonnet-4-20250514", - "max_tokens": 1024, - "messages": [ - { - "role": "user", - "content": "read the foo file" - } - ] -} - --- streaming -- -event: message_start -data: {"type":"message_start","message":{"id":"msg_015SQewixvT9s4cABCVvUE6g","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22,"cache_read_input_tokens":13993,"output_tokens":5,"service_tier":"standard"}} } - -event: content_block_start -data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_01RX68weRSquLx6HUTj65iBo","name":"Read","input":{}} } - -event: ping -data: {"type": "ping"} - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/foo"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\"}"} } - -event: content_block_stop -data: {"type":"content_block_stop","index":0 } - -event: message_delta -data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":61} } - -event: message_stop -data: {"type":"message_stop" } - - --- non-streaming -- -{ - "id": "msg_01JHKqEmh7wYuPXqUWUvusfL", - "container": { - "id": "", - "expires_at": "0001-01-01T00:00:00Z" - }, - "content": [ - { - "citations": null, - "text": "I can see there's a file named `foo` in the `/tmp/blah` directory. Let me read it.", - "type": "text", - "id": "", - "input": null, - "name": "", - "content": { - "OfBetaWebSearchResultBlockArray": null, - "OfString": "", - "OfBetaMCPToolResultBlockContent": null, - "error_code": "", - "type": "", - "content": null, - "return_code": 0, - "stderr": "", - "stdout": "" - }, - "tool_use_id": "", - "server_name": "", - "is_error": false, - "file_id": "", - "signature": "", - "thinking": "", - "data": "" - }, - { - "citations": null, - "text": "", - "type": "tool_use", - "id": "toolu_01AusGgY5aKFhzWrFBv9JfHq", - "input": { - "file_path": "/tmp/blah/foo" - }, - "name": "Read", - "content": { - "OfBetaWebSearchResultBlockArray": null, - "OfString": "", - "OfBetaMCPToolResultBlockContent": null, - "error_code": "", - "type": "", - "content": null, - "return_code": 0, - "stderr": "", - "stdout": "" - }, - "tool_use_id": "", - "server_name": "", - "is_error": false, - "file_id": "", - "signature": "", - "thinking": "", - "data": "" - } - ], - "model": "claude-sonnet-4-20250514", - "role": "assistant", - "stop_reason": "tool_use", - "stop_sequence": "", - "type": "message", - "usage": { - "cache_creation": { - "ephemeral_1h_input_tokens": 0, - "ephemeral_5m_input_tokens": 0 - }, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 23490, - "input_tokens": 5, - "output_tokens": 84, - "server_tool_use": { - "web_search_requests": 0 - }, - "service_tier": "standard" - } -} - diff --git a/aibridged/fixtures/anthropic/single_injected_tool.txtar b/aibridged/fixtures/anthropic/single_injected_tool.txtar deleted file mode 100644 index 33840730e4f3c..0000000000000 --- a/aibridged/fixtures/anthropic/single_injected_tool.txtar +++ /dev/null @@ -1,163 +0,0 @@ -Coder MCP tools automatically injected. - --- request -- -{ - "model": "claude-sonnet-4-20250514", - "max_tokens": 1024, - "messages": [ - { - "role": "user", - "content": "list coder workspace IDs for admin" - } - ] -} - --- streaming -- -event: message_start -data: {"type":"message_start","message":{"id":"msg_01JWGa2JHsKBHL28Cjr2dvPK","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7545,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5,"service_tier":"standard"}} } - -event: content_block_start -data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } - -event: ping -data: {"type": "ping"} - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll list the work"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"spaces for the admin user to get their"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" workspace IDs."} } - -event: content_block_stop -data: {"type":"content_block_stop","index":0 } - -event: content_block_start -data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01TSQLR6R6wBUqoxGPjQKDAj","name":"__mcp__coder_coder_list_workspaces","input":{}} } - -event: content_block_delta -data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} } - -event: content_block_delta -data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"owner\""} } - -event: content_block_delta -data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":": \"ad"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"min\"}"} } - -event: content_block_stop -data: {"type":"content_block_stop","index":1 } - -event: message_delta -data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":75}} - -event: message_stop -data: {"type":"message_stop" } - - --- streaming/tool-call -- -event: message_start -data: {"type":"message_start","message":{"id":"msg_01LZSVzMCLivzXrp6ZnTcmeG","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7763,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} } - -event: content_block_start -data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } - -event: ping -data: {"type": "ping"} - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Here"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" are the workspace IDs for the admin user:\n\n**"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Admin's Workspaces:**\n- Workspace ID: `dd711d5c-83c"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"6-4c08-a0af-b73055906e8"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"c`\n - Name: `bob`\n - Template: `docker"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"`\n - Template ID: `b3a9d9b4-486a-4"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"f21-8884-d81d5dbdd837`"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\nThe admin user currently has 1 workspace named \"bob\" created from"} } - -event: content_block_delta -data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the \"docker\" template."} } - -event: content_block_stop -data: {"type":"content_block_stop","index":0 } - -event: message_delta -data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":128} } - -event: message_stop -data: {"type":"message_stop" } - - --- non-streaming -- -{ - "id": "msg_01FwkWU26guw9EwkL8zeacPL", - "type": "message", - "role": "assistant", - "model": "claude-sonnet-4-20250514", - "content": [ - { - "type": "text", - "text": "I'll list the workspaces for the admin user to get their workspace IDs." - }, - { - "type": "tool_use", - "id": "toolu_01QjNz5b3HxAqAccTVnSMsKP", - "name": "__mcp__coder_coder_list_workspaces", - "input": { - "owner": "admin" - } - } - ], - "stop_reason": "tool_use", - "stop_sequence": null, - "usage": { - "input_tokens": 7545, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - "output_tokens": 75, - "service_tier": "standard" - } -} - - --- non-streaming/tool-call -- -{ - "id": "msg_01Sr5BnPSwodTo8Df4XvUBg5", - "type": "message", - "role": "assistant", - "model": "claude-sonnet-4-20250514", - "content": [ - { - "type": "text", - "text": "Here are the Coder workspace IDs for the admin user:\n\n**Workspace ID:** `dd711d5c-83c6-4c08-a0af-b73055906e8c`\n- **Name:** bob\n- **Template:** docker\n- **Template ID:** b3a9d9b4-486a-4f21-8884-d81d5dbdd837\n- **Status:** Up to date (not outdated)\n\nThe admin user currently has 1 workspace named \"bob\" running on the \"docker\" template." - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 7763, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - "output_tokens": 129, - "service_tier": "standard" - } -} - diff --git a/aibridged/fixtures/openai/fallthrough.txtar b/aibridged/fixtures/openai/fallthrough.txtar deleted file mode 100644 index 09812cb6e4a33..0000000000000 --- a/aibridged/fixtures/openai/fallthrough.txtar +++ /dev/null @@ -1,524 +0,0 @@ -API endpoints not explicitly handled will fallthrough to upstream via reverse-proxy. - --- response -- -{ - "object": "list", - "data": [ - { - "id": "gpt-4-0613", - "object": "model", - "created": 1686588896, - "owned_by": "openai" - }, - { - "id": "gpt-4", - "object": "model", - "created": 1687882411, - "owned_by": "openai" - }, - { - "id": "gpt-3.5-turbo", - "object": "model", - "created": 1677610602, - "owned_by": "openai" - }, - { - "id": "gpt-5-nano", - "object": "model", - "created": 1754426384, - "owned_by": "system" - }, - { - "id": "gpt-5", - "object": "model", - "created": 1754425777, - "owned_by": "system" - }, - { - "id": "gpt-5-mini-2025-08-07", - "object": "model", - "created": 1754425867, - "owned_by": "system" - }, - { - "id": "gpt-5-mini", - "object": "model", - "created": 1754425928, - "owned_by": "system" - }, - { - "id": "gpt-5-nano-2025-08-07", - "object": "model", - "created": 1754426303, - "owned_by": "system" - }, - { - "id": "davinci-002", - "object": "model", - "created": 1692634301, - "owned_by": "system" - }, - { - "id": "babbage-002", - "object": "model", - "created": 1692634615, - "owned_by": "system" - }, - { - "id": "gpt-3.5-turbo-instruct", - "object": "model", - "created": 1692901427, - "owned_by": "system" - }, - { - "id": "gpt-3.5-turbo-instruct-0914", - "object": "model", - "created": 1694122472, - "owned_by": "system" - }, - { - "id": "dall-e-3", - "object": "model", - "created": 1698785189, - "owned_by": "system" - }, - { - "id": "dall-e-2", - "object": "model", - "created": 1698798177, - "owned_by": "system" - }, - { - "id": "gpt-4-1106-preview", - "object": "model", - "created": 1698957206, - "owned_by": "system" - }, - { - "id": "gpt-3.5-turbo-1106", - "object": "model", - "created": 1698959748, - "owned_by": "system" - }, - { - "id": "tts-1-hd", - "object": "model", - "created": 1699046015, - "owned_by": "system" - }, - { - "id": "tts-1-1106", - "object": "model", - "created": 1699053241, - "owned_by": "system" - }, - { - "id": "tts-1-hd-1106", - "object": "model", - "created": 1699053533, - "owned_by": "system" - }, - { - "id": "text-embedding-3-small", - "object": "model", - "created": 1705948997, - "owned_by": "system" - }, - { - "id": "text-embedding-3-large", - "object": "model", - "created": 1705953180, - "owned_by": "system" - }, - { - "id": "gpt-4-0125-preview", - "object": "model", - "created": 1706037612, - "owned_by": "system" - }, - { - "id": "gpt-4-turbo-preview", - "object": "model", - "created": 1706037777, - "owned_by": "system" - }, - { - "id": "gpt-3.5-turbo-0125", - "object": "model", - "created": 1706048358, - "owned_by": "system" - }, - { - "id": "gpt-4-turbo", - "object": "model", - "created": 1712361441, - "owned_by": "system" - }, - { - "id": "gpt-4-turbo-2024-04-09", - "object": "model", - "created": 1712601677, - "owned_by": "system" - }, - { - "id": "gpt-4o", - "object": "model", - "created": 1715367049, - "owned_by": "system" - }, - { - "id": "gpt-4o-2024-05-13", - "object": "model", - "created": 1715368132, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini-2024-07-18", - "object": "model", - "created": 1721172717, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini", - "object": "model", - "created": 1721172741, - "owned_by": "system" - }, - { - "id": "gpt-4o-2024-08-06", - "object": "model", - "created": 1722814719, - "owned_by": "system" - }, - { - "id": "chatgpt-4o-latest", - "object": "model", - "created": 1723515131, - "owned_by": "system" - }, - { - "id": "o1-mini-2024-09-12", - "object": "model", - "created": 1725648979, - "owned_by": "system" - }, - { - "id": "o1-mini", - "object": "model", - "created": 1725649008, - "owned_by": "system" - }, - { - "id": "gpt-4o-realtime-preview-2024-10-01", - "object": "model", - "created": 1727131766, - "owned_by": "system" - }, - { - "id": "gpt-4o-audio-preview-2024-10-01", - "object": "model", - "created": 1727389042, - "owned_by": "system" - }, - { - "id": "gpt-4o-audio-preview", - "object": "model", - "created": 1727460443, - "owned_by": "system" - }, - { - "id": "gpt-4o-realtime-preview", - "object": "model", - "created": 1727659998, - "owned_by": "system" - }, - { - "id": "omni-moderation-latest", - "object": "model", - "created": 1731689265, - "owned_by": "system" - }, - { - "id": "omni-moderation-2024-09-26", - "object": "model", - "created": 1732734466, - "owned_by": "system" - }, - { - "id": "gpt-4o-realtime-preview-2024-12-17", - "object": "model", - "created": 1733945430, - "owned_by": "system" - }, - { - "id": "gpt-4o-audio-preview-2024-12-17", - "object": "model", - "created": 1734034239, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini-realtime-preview-2024-12-17", - "object": "model", - "created": 1734112601, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini-audio-preview-2024-12-17", - "object": "model", - "created": 1734115920, - "owned_by": "system" - }, - { - "id": "o1-2024-12-17", - "object": "model", - "created": 1734326976, - "owned_by": "system" - }, - { - "id": "o1", - "object": "model", - "created": 1734375816, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini-realtime-preview", - "object": "model", - "created": 1734387380, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini-audio-preview", - "object": "model", - "created": 1734387424, - "owned_by": "system" - }, - { - "id": "o3-mini", - "object": "model", - "created": 1737146383, - "owned_by": "system" - }, - { - "id": "o3-mini-2025-01-31", - "object": "model", - "created": 1738010200, - "owned_by": "system" - }, - { - "id": "gpt-4o-2024-11-20", - "object": "model", - "created": 1739331543, - "owned_by": "system" - }, - { - "id": "gpt-4o-search-preview-2025-03-11", - "object": "model", - "created": 1741388170, - "owned_by": "system" - }, - { - "id": "gpt-4o-search-preview", - "object": "model", - "created": 1741388720, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini-search-preview-2025-03-11", - "object": "model", - "created": 1741390858, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini-search-preview", - "object": "model", - "created": 1741391161, - "owned_by": "system" - }, - { - "id": "gpt-4o-transcribe", - "object": "model", - "created": 1742068463, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini-transcribe", - "object": "model", - "created": 1742068596, - "owned_by": "system" - }, - { - "id": "o1-pro-2025-03-19", - "object": "model", - "created": 1742251504, - "owned_by": "system" - }, - { - "id": "o1-pro", - "object": "model", - "created": 1742251791, - "owned_by": "system" - }, - { - "id": "gpt-4o-mini-tts", - "object": "model", - "created": 1742403959, - "owned_by": "system" - }, - { - "id": "o3-2025-04-16", - "object": "model", - "created": 1744133301, - "owned_by": "system" - }, - { - "id": "o4-mini-2025-04-16", - "object": "model", - "created": 1744133506, - "owned_by": "system" - }, - { - "id": "o3", - "object": "model", - "created": 1744225308, - "owned_by": "system" - }, - { - "id": "o4-mini", - "object": "model", - "created": 1744225351, - "owned_by": "system" - }, - { - "id": "gpt-4.1-2025-04-14", - "object": "model", - "created": 1744315746, - "owned_by": "system" - }, - { - "id": "gpt-4.1", - "object": "model", - "created": 1744316542, - "owned_by": "system" - }, - { - "id": "gpt-4.1-mini-2025-04-14", - "object": "model", - "created": 1744317547, - "owned_by": "system" - }, - { - "id": "gpt-4.1-mini", - "object": "model", - "created": 1744318173, - "owned_by": "system" - }, - { - "id": "gpt-4.1-nano-2025-04-14", - "object": "model", - "created": 1744321025, - "owned_by": "system" - }, - { - "id": "gpt-4.1-nano", - "object": "model", - "created": 1744321707, - "owned_by": "system" - }, - { - "id": "gpt-image-1", - "object": "model", - "created": 1745517030, - "owned_by": "system" - }, - { - "id": "codex-mini-latest", - "object": "model", - "created": 1746673257, - "owned_by": "system" - }, - { - "id": "o3-pro", - "object": "model", - "created": 1748475349, - "owned_by": "system" - }, - { - "id": "gpt-4o-realtime-preview-2025-06-03", - "object": "model", - "created": 1748907838, - "owned_by": "system" - }, - { - "id": "gpt-4o-audio-preview-2025-06-03", - "object": "model", - "created": 1748908498, - "owned_by": "system" - }, - { - "id": "o3-pro-2025-06-10", - "object": "model", - "created": 1749166761, - "owned_by": "system" - }, - { - "id": "o4-mini-deep-research", - "object": "model", - "created": 1749685485, - "owned_by": "system" - }, - { - "id": "o3-deep-research", - "object": "model", - "created": 1749840121, - "owned_by": "system" - }, - { - "id": "o3-deep-research-2025-06-26", - "object": "model", - "created": 1750865219, - "owned_by": "system" - }, - { - "id": "o4-mini-deep-research-2025-06-26", - "object": "model", - "created": 1750866121, - "owned_by": "system" - }, - { - "id": "gpt-5-chat-latest", - "object": "model", - "created": 1754073306, - "owned_by": "system" - }, - { - "id": "gpt-5-2025-08-07", - "object": "model", - "created": 1754075360, - "owned_by": "system" - }, - { - "id": "gpt-3.5-turbo-16k", - "object": "model", - "created": 1683758102, - "owned_by": "openai-internal" - }, - { - "id": "tts-1", - "object": "model", - "created": 1681940951, - "owned_by": "openai-internal" - }, - { - "id": "whisper-1", - "object": "model", - "created": 1677532384, - "owned_by": "openai-internal" - }, - { - "id": "text-embedding-ada-002", - "object": "model", - "created": 1671217299, - "owned_by": "openai-internal" - } - ] -} diff --git a/aibridged/fixtures/openai/simple.txtar b/aibridged/fixtures/openai/simple.txtar deleted file mode 100644 index 55cb3b91fea58..0000000000000 --- a/aibridged/fixtures/openai/simple.txtar +++ /dev/null @@ -1,536 +0,0 @@ -Simple request. - --- request -- -{ - "messages": [ - { - "role": "user", - "content": "how many angels can dance on the head of a pin\n" - } - ], - "model": "gpt-4.1" -} - --- streaming -- -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"How"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" many"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" angels"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" dance"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" on"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" head"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" pin"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"?\""},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" classic"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" example"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ph"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ilos"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"oph"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ical"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theological"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" r"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"iddle"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**,"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" not"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" genuine"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" inquiry"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" metaph"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ysical"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" realities"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" The"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" phrase"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" most"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" likely"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" originated"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" during"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"med"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ieval"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" schol"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"astic"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debates"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**,"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" where"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" scholars"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" engaged"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" complex"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" discussions"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" nature"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" spiritual"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" beings"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" limits"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" human"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" knowledge"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Meaning"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Context"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Not"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" meant"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" have"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" literal"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" answer"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Angels"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Christian"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theology"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" are"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" spiritual"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" ("},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"not"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" physical"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":")"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" beings"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" so"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" they"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" don"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"’t"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" occupy"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" space"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" physical"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" sense"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Symbol"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ic"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" purpose"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" The"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" often"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" used"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" mock"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" illustrate"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" arguments"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" perceived"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" overly"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" speculative"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" irrelevant"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Answers"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\""},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" through"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" History"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Sch"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ol"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ast"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ics"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" There's"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" little"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" evidence"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" medieval"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" scholars"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" literally"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debated"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" this"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":";"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" it's"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" later"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"car"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ic"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ature"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" their"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" intricate"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theological"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" arguments"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Modern"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" usage"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" It's"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" cited"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" an"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" example"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" pointless"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" un"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"answer"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"able"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Summary"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"There"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" no"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" specific"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" number"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":";"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" rhetorical"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" highlighting"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" limits"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theoretical"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" speculative"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" reasoning"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Would"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" like"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" know"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" medieval"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" schol"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"astic"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debates"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" how"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" this"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" used"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" modern"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" discourse"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} - -data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":19,"completion_tokens":238,"total_tokens":257,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} - -data: [DONE] - --- non-streaming -- -{ - "id": "chatcmpl-BwojtGkriucZoAj99ylsoapwHo5V2", - "object": "chat.completion", - "created": 1753357765, - "model": "gpt-4.1-2025-04-14", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "The question \"How many angels can dance on the head of a pin?\" is a classic example of a rhetorical or philosophical question—*not* a real theological inquiry.\n\n**Origin and Meaning:**\n- The phrase is used to lampoon or satirize overly subtle, speculative, or irrelevant philosophical debates, especially those attributed to medieval scholasticism.\n- There is **no actual historical record** of medieval theologians debating this specific question.\n- It **illustrates debates about the nature of angels**—whether they occupy physical space, for example—but not in such literal terms.\n\n**If answered literally:**\n- If angels are considered non-corporeal and not limited by physical space, **an infinite number** could \"dance\" on the head of a pin.\n- If taken as a joke, the answer is up to the storyteller!\n\n**In summary:** \nIt's a facetious question highlighting the limits or absurdities of some philosophical or theological arguments. There is no fixed answer.", - "refusal": null, - "annotations": [] - }, - "logprobs": null, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 19, - "completion_tokens": 200, - "total_tokens": 219, - "prompt_tokens_details": { - "cached_tokens": 0, - "audio_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0, - "audio_tokens": 0, - "accepted_prediction_tokens": 0, - "rejected_prediction_tokens": 0 - } - }, - "service_tier": "default", - "system_fingerprint": "fp_b3f1157249" -} - diff --git a/aibridged/fixtures/openai/single_builtin_tool.txtar b/aibridged/fixtures/openai/single_builtin_tool.txtar deleted file mode 100644 index 0eae82126a0e2..0000000000000 --- a/aibridged/fixtures/openai/single_builtin_tool.txtar +++ /dev/null @@ -1,102 +0,0 @@ -LLM (https://llm.datasette.io/) configured with a simple "read_file" tool. - --- request -- -{ - "messages": [ - { - "role": "user", - "content": "how large is the README.md file in my current path" - } - ], - "model": "gpt-4.1", - "tools": [ - { - "type": "function", - "function": { - "name": "read_file", - "description": "Read the contents of a file at the given path.", - "parameters": { - "properties": { - "path": { - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - } - } - } - ] -} - --- streaming -- -data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_HjeqP7YeRkoNj0de9e3U4X4B","type":"function","function":{"name":"read_file","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"path"}}]},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"README"}}]},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":".md"}}]},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null} - -data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} - -data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[],"usage":{"prompt_tokens":60,"completion_tokens":15,"total_tokens":75,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} - -data: [DONE] - --- non-streaming -- -{ - "id": "chatcmpl-BwkyFElDIr1egmFyfQ9z4vPBto7m2", - "object": "chat.completion", - "created": 1753343279, - "model": "gpt-4.1-2025-04-14", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": null, - "tool_calls": [ - { - "id": "call_KjzAbhiZC6nk81tQzL7pwlpc", - "type": "function", - "function": { - "name": "read_file", - "arguments": "{\"path\":\"README.md\"}" - } - } - ], - "refusal": null, - "annotations": [] - }, - "logprobs": null, - "finish_reason": "tool_calls" - } - ], - "usage": { - "prompt_tokens": 60, - "completion_tokens": 15, - "total_tokens": 75, - "prompt_tokens_details": { - "cached_tokens": 0, - "audio_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0, - "audio_tokens": 0, - "accepted_prediction_tokens": 0, - "rejected_prediction_tokens": 0 - } - }, - "service_tier": "default", - "system_fingerprint": "fp_b3f1157249" -} - diff --git a/aibridged/fixtures/openai/single_injected_tool.txtar b/aibridged/fixtures/openai/single_injected_tool.txtar deleted file mode 100644 index b9670fc2323b3..0000000000000 --- a/aibridged/fixtures/openai/single_injected_tool.txtar +++ /dev/null @@ -1,294 +0,0 @@ -Coder MCP tools automatically injected. - --- request -- -{ - "model": "gpt-4.1", - "messages": [ - { - "role": "user", - "content": "list coder workspace IDs for admin" - } - ] -} - --- streaming -- -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ha7QSWuIrCLSg"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TxlRNztDyni152"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" am"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"d8rQaibDQpyL"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Qlbfp6UEp"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"68rb1Vo3ymBh"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" call"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"i7c6mc6zJY"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Z9syl1x73E7"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" appropriate"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5wK"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" tool"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"qxf0biXh4i"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UMXRLeWr9r7g"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" list"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PkO0yHjNu3"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" all"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ktUBR7vT2FC"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" work"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xdNr1gCRJW"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"spaces"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5z5luvhUz"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"G6D7Ze3OlLR"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6BZ54FOiuA7"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6b0xOBQj2J"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" admin"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"X5gzNDQyO"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"oSONGErPa7g"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" display"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"EK9oGdN"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" their"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TPtBmjMIt"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" IDs"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FONB73iSePd"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"VMpWnam5jp"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_0TxntkwDB66KH8z4RwNqeWrZ","type":"function","function":{"name":"__mcp__coder_coder_list_workspaces","arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kY"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"n5"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"owner"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"admin"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"1t"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null,"obfuscation":"sDj"} - -data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[],"usage":{"prompt_tokens":4862,"completion_tokens":45,"total_tokens":4907,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"8sIWE1chOW"} - -data: [DONE] - - --- streaming/tool-call -- -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"DBu9uyty0Uhux"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"Here"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Pk0tDwr0wkd"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" are"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ACu9WW1Lsz4"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xrXWRUKKAZl"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LowCw"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" IDs"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RXNpYewll1k"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WnyxJrani1M"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"JrnDAJOLap4"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RNZIdDo4vj"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" admin"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nJ7O0qcsG"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"0k0UVPjnE2"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dtGIleZ8Nl9lU7"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wKNWu"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Name"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cmzvcWMEIp"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GsImQO12UCnPHY"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" bob"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AR4Jvn87StW"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WoNeyT7BKKjIS"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2Ou4DytumVPlyW"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PRWw3"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" ID"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"rrKKjluNdVET"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"v6NUOTV1Pd6piU"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" dd"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UuYGjaLT7OXO"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"711"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vLHjJVhbJgec"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"d"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2yDtuCir4L9eyS"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"5"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kyJOHcdfo1NMrP"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nuKRieC0bpf6O3"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"q29JHHRnNg1GYt"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"83"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"e0o7Zu6eKnter"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NCASF3SYR9GDQl"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"6"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"eG48V9XgxodtbB"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"CpP8ALTDfT0yBv"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"4"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"uQY85IhRAfuFl9"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wsdJSv3bN65S5a"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"08"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dq2JARx8gsgIm"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-a"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"4booyOM91IZdC"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"0"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wVJJDjNFBXO3OC"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"af"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"XFtDbXdnHdnF3"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-b"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"juymtEmZxo1Ez"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"730"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"8pIOLoJZJAfe"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"559"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NPfQJmrtGPlY"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"06"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"jsqxOojcWTY3A"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"e"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cWYFwWie0ciIju"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"8"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ilVWzWQLUWQOMw"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ea99MtCCypPar2"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SDq7UD3LcH7"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"Let"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"S343Ji05lUgD"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" me"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TTCD9vPg98sO"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" know"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xcsP3lRI6f"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" if"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"bS0qh0vq73n3"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"pxUYdxCHoy8"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" need"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wjLDXO4uD8"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"B6ckyharjv"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" information"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xrN"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"aqv4RrWxJ"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" any"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hqdG5QSND4E"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"HvfgjMOXU6aG"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" these"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"yE0jSPMkD"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" work"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wWfGxJR2wt"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"spaces"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hOXndth8X"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"MReMwESHIpaDyo"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"EFeFvdS8m"} - -data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[],"usage":{"prompt_tokens":5049,"completion_tokens":60,"total_tokens":5109,"prompt_tokens_details":{"cached_tokens":4864,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"0JQt7Fw"} - -data: [DONE] - - --- non-streaming -- -{ - "id": "chatcmpl-C1XAKDTVYnmWS7tgvg7vPje00PIiy", - "object": "chat.completion", - "created": 1754481852, - "model": "gpt-4.1-2025-04-14", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "I am about to call the relevant function to list all workspaces for the user admin and provide their workspace IDs.\n\nExecuting the function call now.", - "tool_calls": [ - { - "id": "call_aEuQAWKQYInC6fQ4z0iatdVP", - "type": "function", - "function": { - "name": "__mcp__coder_coder_list_workspaces", - "arguments": "{\"owner\":\"admin\"}" - } - } - ], - "refusal": null, - "annotations": [] - }, - "logprobs": null, - "finish_reason": "tool_calls" - } - ], - "usage": { - "prompt_tokens": 4862, - "completion_tokens": 52, - "total_tokens": 4914, - "prompt_tokens_details": { - "cached_tokens": 0, - "audio_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0, - "audio_tokens": 0, - "accepted_prediction_tokens": 0, - "rejected_prediction_tokens": 0 - } - }, - "service_tier": "default", - "system_fingerprint": "fp_51e1070cf2" -} - - --- non-streaming/tool-call -- -{ - "id": "chatcmpl-C1XANLwdflVxAjKOjbMP3LJxSlXsS", - "object": "chat.completion", - "created": 1754481855, - "model": "gpt-4.1-2025-04-14", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "Here is the list of Coder workspace IDs for the user admin:\n\n- Workspace Name: bob\n- Workspace ID: dd711d5c-83c6-4c08-a0af-b73055906e8c\n\nLet me know if you need more details or actions on this workspace!", - "refusal": null, - "annotations": [] - }, - "logprobs": null, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 5056, - "completion_tokens": 63, - "total_tokens": 5119, - "prompt_tokens_details": { - "cached_tokens": 4864, - "audio_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0, - "audio_tokens": 0, - "accepted_prediction_tokens": 0, - "rejected_prediction_tokens": 0 - } - }, - "service_tier": "default", - "system_fingerprint": "fp_51e1070cf2" -} - diff --git a/aibridged/mcp.go b/aibridged/mcp.go deleted file mode 100644 index 2cff5dee85186..0000000000000 --- a/aibridged/mcp.go +++ /dev/null @@ -1,170 +0,0 @@ -package aibridged - -import ( - "context" - "fmt" - "strings" - - "github.com/mark3labs/mcp-go/client" - "github.com/mark3labs/mcp-go/client/transport" - "github.com/mark3labs/mcp-go/mcp" - "golang.org/x/exp/maps" - "golang.org/x/xerrors" - - "cdr.dev/slog" -) - -type MCPToolBridge struct { - name string - client *client.Client - logger slog.Logger - foundTools map[string]*MCPTool -} - -type MCPTool struct { - client *client.Client - ID string - Name string - Description string - Params map[string]any - Required []string -} - -const ( - MCPPrefix = "__mcp__" - MCPDelimiter = "_" // TODO: ensure server names CANNOT contain this char. -) - -func NewMCPToolBridge(name, serverURL string, headers map[string]string, logger slog.Logger) (*MCPToolBridge, error) { - // ts := transport.NewMemoryTokenStore() - // if err := ts.SaveToken(&transport.Token{ - // AccessToken: token, - // }); err != nil { - // return nil, xerrors.Errorf("save token: %w", err) - //} - - mcpClient, err := client.NewStreamableHttpClient(serverURL, - transport.WithHTTPHeaders(headers)) - // transport.WithHTTPOAuth(transport.OAuthConfig{ - // TokenStore: ts, - // })) - if err != nil { - return nil, xerrors.Errorf("create streamable http client: %w", err) - } - - return &MCPToolBridge{ - name: name, - client: mcpClient, - logger: logger, - }, nil -} - -func (b *MCPToolBridge) Init(ctx context.Context) error { - if err := b.client.Start(ctx); err != nil { - return xerrors.Errorf("start client: %w", err) - } - - tools, err := b.fetchMCPTools(ctx) - if err != nil { - return xerrors.Errorf("fetch tools: %w", err) - } - - b.foundTools = tools - return nil -} - -func (b *MCPToolBridge) ListTools() []*MCPTool { - return maps.Values(b.foundTools) -} - -func (b *MCPToolBridge) HasTool(name string) bool { - if b.foundTools == nil { - return false - } - - _, ok := b.foundTools[name] - return ok -} - -func (b *MCPToolBridge) CallTool(ctx context.Context, name string, input any) (*mcp.CallToolResult, error) { - return b.client.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: name, - Arguments: input, - }, - }) -} - -func (t *MCPTool) Call(ctx context.Context, input any) (*mcp.CallToolResult, error) { - if t == nil { - return nil, xerrors.Errorf("nil tool!") - } - - return t.client.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: t.Name, - Arguments: input, - }, - }) -} - -func (b *MCPToolBridge) fetchMCPTools(ctx context.Context) (map[string]*MCPTool, error) { - initReq := mcp.InitializeRequest{ - Params: mcp.InitializeParams{ - ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, - ClientInfo: mcp.Implementation{ - Name: "coder-ai-bridge", - Version: "0.0.1", - }, - }, - } - - result, err := b.client.Initialize(ctx, initReq) - if err != nil { - return nil, xerrors.Errorf("init MCP client: %w", err) - } - b.logger.Debug(ctx, "mcp client initialized", slog.F("name", result.ServerInfo.Name), slog.F("server_version", result.ServerInfo.Version)) - - // Test tool listing - tools, err := b.client.ListTools(ctx, mcp.ListToolsRequest{}) - if err != nil { - return nil, xerrors.Errorf("list MCP tools: %w", err) - } - - out := make(map[string]*MCPTool, len(tools.Tools)) - for _, tool := range tools.Tools { - out[tool.Name] = &MCPTool{ - client: b.client, - ID: EncodeToolID(b.name, tool.Name), - Name: tool.Name, - Description: tool.Description, - Params: tool.InputSchema.Properties, - Required: tool.InputSchema.Required, - } - } - return out, nil -} - -// EncodeToolID namespaces the given tool name to disambiguate against other tools. -func EncodeToolID(server, tool string) string { - return fmt.Sprintf("%s%s%s%s", MCPPrefix, server, MCPDelimiter, tool) -} - -// DecodeToolID strips the namespacing from EncodeToolID. -func DecodeToolID(id string) (server string, tool string, err error) { - _, name, ok := strings.Cut(id, MCPPrefix) - if !ok { - return "", "", xerrors.Errorf("unable to decode %q, prefix %q not found", id, MCPPrefix) - } - - server, tool, ok = strings.Cut(name, MCPDelimiter) - if !ok { - return "", "", xerrors.Errorf("unable to decode %q, delimiter %q not found", id, MCPDelimiter) - } - - return server, tool, nil -} - -func (b *MCPToolBridge) Close() { - // TODO: atomically close. -} diff --git a/aibridged/middleware.go b/aibridged/middleware.go index 34a9472ee4f62..da8bf089ea10b 100644 --- a/aibridged/middleware.go +++ b/aibridged/middleware.go @@ -6,15 +6,11 @@ import ( "crypto/subtle" "net/http" + "github.com/coder/aibridge" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpmw" ) -type ( - ContextKeyBridgeAPIKey struct{} - ContextKeyBridgeUserID struct{} -) - // AuthMiddleware extracts and validates authorization tokens for AI bridge endpoints. // It supports both Bearer tokens in Authorization headers and Coder session tokens // from cookies/headers following the same patterns as existing Coder authentication. @@ -31,7 +27,7 @@ func AuthMiddleware(db database.Store) func(http.Handler) http.Handler { } // Validate token using httpmw.APIKeyFromRequest - key, _, ok := httpmw.APIKeyFromRequest(ctx, db, func(r *http.Request) string { + key, _, ok := httpmw.APIKeyFromRequest(ctx, db, func(*http.Request) string { return token }, &http.Request{}) @@ -41,8 +37,8 @@ func AuthMiddleware(db database.Store) func(http.Handler) http.Handler { } ctx = context.WithValue( - context.WithValue(ctx, ContextKeyBridgeUserID{}, key.UserID), - ContextKeyBridgeAPIKey{}, token) + context.WithValue(ctx, aibridge.ContextKeyBridgeUserID{}, key.UserID), + aibridge.ContextKeyBridgeAPIKey{}, token) // Pass request with modify context including the request token. next.ServeHTTP(rw, r.WithContext(ctx)) diff --git a/aibridged/openai.go b/aibridged/openai.go deleted file mode 100644 index 25bc157cbda32..0000000000000 --- a/aibridged/openai.go +++ /dev/null @@ -1,96 +0,0 @@ -package aibridged - -import ( - "regexp" - "strings" - - "github.com/openai/openai-go" - "github.com/openai/openai-go/packages/param" - "github.com/tidwall/gjson" - "golang.org/x/xerrors" - "tailscale.com/types/ptr" -) - -// ChatCompletionNewParamsWrapper exists because the "stream" param is not included in openai.ChatCompletionNewParams. -type ChatCompletionNewParamsWrapper struct { - openai.ChatCompletionNewParams `json:""` - Stream bool `json:"stream,omitempty"` -} - -func (c ChatCompletionNewParamsWrapper) MarshalJSON() ([]byte, error) { - type shadow ChatCompletionNewParamsWrapper - return param.MarshalWithExtras(c, (*shadow)(&c), map[string]any{ - "stream": c.Stream, - }) -} - -func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { - err := c.ChatCompletionNewParams.UnmarshalJSON(raw) - if err != nil { - return err - } - - in := gjson.ParseBytes(raw) - if stream := in.Get("stream"); stream.Exists() { - c.Stream = stream.Bool() - if c.Stream { - c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{ - IncludeUsage: openai.Bool(true), // Always include usage when streaming. - } - } else { - c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} - } - } else { - c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} - } - - return nil -} - -func (c *ChatCompletionNewParamsWrapper) LastUserPrompt() (*string, error) { - if c == nil { - return nil, xerrors.New("nil struct") - } - - if len(c.Messages) == 0 { - return nil, xerrors.New("no messages") - } - - var msg *openai.ChatCompletionUserMessageParam - for i := len(c.Messages) - 1; i >= 0; i-- { - m := c.Messages[i] - if m.OfUser != nil { - msg = m.OfUser - break - } - } - - if msg == nil { - return nil, nil - } - - userMessage := msg.Content.OfString.String() - if isCursor, _ := regexp.MatchString("", userMessage); isCursor { - userMessage = extractCursorUserQuery(userMessage) - } - - return ptr.To(strings.TrimSpace(userMessage)), nil -} - -func sumUsage(ref, in openai.CompletionUsage) openai.CompletionUsage { - return openai.CompletionUsage{ - CompletionTokens: ref.CompletionTokens + in.CompletionTokens, - PromptTokens: ref.PromptTokens + in.PromptTokens, - TotalTokens: ref.TotalTokens + in.TotalTokens, - CompletionTokensDetails: openai.CompletionUsageCompletionTokensDetails{ - AcceptedPredictionTokens: ref.CompletionTokensDetails.AcceptedPredictionTokens + in.CompletionTokensDetails.AcceptedPredictionTokens, - AudioTokens: ref.CompletionTokensDetails.AudioTokens + in.CompletionTokensDetails.AudioTokens, - ReasoningTokens: ref.CompletionTokensDetails.ReasoningTokens + in.CompletionTokensDetails.ReasoningTokens, - RejectedPredictionTokens: ref.CompletionTokensDetails.RejectedPredictionTokens + in.CompletionTokensDetails.RejectedPredictionTokens, - }, - PromptTokensDetails: openai.CompletionUsagePromptTokensDetails{ - AudioTokens: ref.PromptTokensDetails.AudioTokens + in.PromptTokensDetails.AudioTokens, - CachedTokens: ref.PromptTokensDetails.CachedTokens + in.PromptTokensDetails.CachedTokens, - }, - } -} diff --git a/aibridged/proto/aibridged.pb.go b/aibridged/proto/aibridged.pb.go deleted file mode 100644 index 08c43c98a5bd5..0000000000000 --- a/aibridged/proto/aibridged.pb.go +++ /dev/null @@ -1,761 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.30.0 -// protoc v4.23.4 -// source: aibridged/proto/aibridged.proto - -package proto - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - anypb "google.golang.org/protobuf/types/known/anypb" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type StartSessionRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - InitiatorId string `protobuf:"bytes,2,opt,name=initiator_id,json=initiatorId,proto3" json:"initiator_id,omitempty"` - Provider string `protobuf:"bytes,3,opt,name=provider,proto3" json:"provider,omitempty"` - Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` -} - -func (x *StartSessionRequest) Reset() { - *x = StartSessionRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *StartSessionRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StartSessionRequest) ProtoMessage() {} - -func (x *StartSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StartSessionRequest.ProtoReflect.Descriptor instead. -func (*StartSessionRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} -} - -func (x *StartSessionRequest) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *StartSessionRequest) GetInitiatorId() string { - if x != nil { - return x.InitiatorId - } - return "" -} - -func (x *StartSessionRequest) GetProvider() string { - if x != nil { - return x.Provider - } - return "" -} - -func (x *StartSessionRequest) GetModel() string { - if x != nil { - return x.Model - } - return "" -} - -type StartSessionResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *StartSessionResponse) Reset() { - *x = StartSessionResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *StartSessionResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StartSessionResponse) ProtoMessage() {} - -func (x *StartSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StartSessionResponse.ProtoReflect.Descriptor instead. -func (*StartSessionResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} -} - -type TrackTokenUsageRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` - InputTokens int64 `protobuf:"varint,3,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` - OutputTokens int64 `protobuf:"varint,4,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` - Metadata map[string]*anypb.Any `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` -} - -func (x *TrackTokenUsageRequest) Reset() { - *x = TrackTokenUsageRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TrackTokenUsageRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TrackTokenUsageRequest) ProtoMessage() {} - -func (x *TrackTokenUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TrackTokenUsageRequest.ProtoReflect.Descriptor instead. -func (*TrackTokenUsageRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} -} - -func (x *TrackTokenUsageRequest) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *TrackTokenUsageRequest) GetMsgId() string { - if x != nil { - return x.MsgId - } - return "" -} - -func (x *TrackTokenUsageRequest) GetInputTokens() int64 { - if x != nil { - return x.InputTokens - } - return 0 -} - -func (x *TrackTokenUsageRequest) GetOutputTokens() int64 { - if x != nil { - return x.OutputTokens - } - return 0 -} - -func (x *TrackTokenUsageRequest) GetMetadata() map[string]*anypb.Any { - if x != nil { - return x.Metadata - } - return nil -} - -type TrackTokenUsageResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *TrackTokenUsageResponse) Reset() { - *x = TrackTokenUsageResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TrackTokenUsageResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TrackTokenUsageResponse) ProtoMessage() {} - -func (x *TrackTokenUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TrackTokenUsageResponse.ProtoReflect.Descriptor instead. -func (*TrackTokenUsageResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{3} -} - -type TrackUserPromptRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` - Prompt string `protobuf:"bytes,3,opt,name=prompt,proto3" json:"prompt,omitempty"` - Metadata map[string]*anypb.Any `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` -} - -func (x *TrackUserPromptRequest) Reset() { - *x = TrackUserPromptRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TrackUserPromptRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TrackUserPromptRequest) ProtoMessage() {} - -func (x *TrackUserPromptRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TrackUserPromptRequest.ProtoReflect.Descriptor instead. -func (*TrackUserPromptRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{4} -} - -func (x *TrackUserPromptRequest) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *TrackUserPromptRequest) GetMsgId() string { - if x != nil { - return x.MsgId - } - return "" -} - -func (x *TrackUserPromptRequest) GetPrompt() string { - if x != nil { - return x.Prompt - } - return "" -} - -func (x *TrackUserPromptRequest) GetMetadata() map[string]*anypb.Any { - if x != nil { - return x.Metadata - } - return nil -} - -type TrackUserPromptResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *TrackUserPromptResponse) Reset() { - *x = TrackUserPromptResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TrackUserPromptResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TrackUserPromptResponse) ProtoMessage() {} - -func (x *TrackUserPromptResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TrackUserPromptResponse.ProtoReflect.Descriptor instead. -func (*TrackUserPromptResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} -} - -type TrackToolUsageRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - MsgId string `protobuf:"bytes,2,opt,name=msg_id,json=msgId,proto3" json:"msg_id,omitempty"` - Tool string `protobuf:"bytes,3,opt,name=tool,proto3" json:"tool,omitempty"` - Input string `protobuf:"bytes,4,opt,name=input,proto3" json:"input,omitempty"` - Injected bool `protobuf:"varint,5,opt,name=injected,proto3" json:"injected,omitempty"` - Metadata map[string]*anypb.Any `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` -} - -func (x *TrackToolUsageRequest) Reset() { - *x = TrackToolUsageRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TrackToolUsageRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TrackToolUsageRequest) ProtoMessage() {} - -func (x *TrackToolUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TrackToolUsageRequest.ProtoReflect.Descriptor instead. -func (*TrackToolUsageRequest) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{6} -} - -func (x *TrackToolUsageRequest) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *TrackToolUsageRequest) GetMsgId() string { - if x != nil { - return x.MsgId - } - return "" -} - -func (x *TrackToolUsageRequest) GetTool() string { - if x != nil { - return x.Tool - } - return "" -} - -func (x *TrackToolUsageRequest) GetInput() string { - if x != nil { - return x.Input - } - return "" -} - -func (x *TrackToolUsageRequest) GetInjected() bool { - if x != nil { - return x.Injected - } - return false -} - -func (x *TrackToolUsageRequest) GetMetadata() map[string]*anypb.Any { - if x != nil { - return x.Metadata - } - return nil -} - -type TrackToolUsageResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *TrackToolUsageResponse) Reset() { - *x = TrackToolUsageResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TrackToolUsageResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TrackToolUsageResponse) ProtoMessage() {} - -func (x *TrackToolUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_aibridged_proto_aibridged_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TrackToolUsageResponse.ProtoReflect.Descriptor instead. -func (*TrackToolUsageResponse) Descriptor() ([]byte, []int) { - return file_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{7} -} - -var File_aibridged_proto_aibridged_proto protoreflect.FileDescriptor - -var file_aibridged_proto_aibridged_proto_rawDesc = []byte{ - 0x0a, 0x1f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x09, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x1a, 0x19, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, - 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x89, 0x01, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x72, - 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x21, - 0x0a, 0x0c, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x49, - 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, - 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, - 0x64, 0x65, 0x6c, 0x22, 0x16, 0x0a, 0x14, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb6, 0x02, 0x0a, 0x16, - 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, - 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, - 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x4b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, - 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x86, 0x02, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, - 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x4b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x69, 0x62, - 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, - 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x19, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0xb2, 0x02, 0x0a, 0x15, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, - 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, - 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, - 0x67, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, - 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x4a, 0x0a, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x69, - 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, - 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x18, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x32, 0xec, 0x02, 0x0a, 0x0e, 0x41, 0x49, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x44, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x4f, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, - 0x64, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, - 0x64, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, - 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x58, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, - 0x6d, 0x70, 0x74, 0x12, 0x21, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, - 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, - 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6d, - 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x54, 0x72, - 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x6f, - 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, - 0x2e, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, - 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_aibridged_proto_aibridged_proto_rawDescOnce sync.Once - file_aibridged_proto_aibridged_proto_rawDescData = file_aibridged_proto_aibridged_proto_rawDesc -) - -func file_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { - file_aibridged_proto_aibridged_proto_rawDescOnce.Do(func() { - file_aibridged_proto_aibridged_proto_rawDescData = protoimpl.X.CompressGZIP(file_aibridged_proto_aibridged_proto_rawDescData) - }) - return file_aibridged_proto_aibridged_proto_rawDescData -} - -var file_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 11) -var file_aibridged_proto_aibridged_proto_goTypes = []interface{}{ - (*StartSessionRequest)(nil), // 0: aibridged.StartSessionRequest - (*StartSessionResponse)(nil), // 1: aibridged.StartSessionResponse - (*TrackTokenUsageRequest)(nil), // 2: aibridged.TrackTokenUsageRequest - (*TrackTokenUsageResponse)(nil), // 3: aibridged.TrackTokenUsageResponse - (*TrackUserPromptRequest)(nil), // 4: aibridged.TrackUserPromptRequest - (*TrackUserPromptResponse)(nil), // 5: aibridged.TrackUserPromptResponse - (*TrackToolUsageRequest)(nil), // 6: aibridged.TrackToolUsageRequest - (*TrackToolUsageResponse)(nil), // 7: aibridged.TrackToolUsageResponse - nil, // 8: aibridged.TrackTokenUsageRequest.MetadataEntry - nil, // 9: aibridged.TrackUserPromptRequest.MetadataEntry - nil, // 10: aibridged.TrackToolUsageRequest.MetadataEntry - (*anypb.Any)(nil), // 11: google.protobuf.Any -} -var file_aibridged_proto_aibridged_proto_depIdxs = []int32{ - 8, // 0: aibridged.TrackTokenUsageRequest.metadata:type_name -> aibridged.TrackTokenUsageRequest.MetadataEntry - 9, // 1: aibridged.TrackUserPromptRequest.metadata:type_name -> aibridged.TrackUserPromptRequest.MetadataEntry - 10, // 2: aibridged.TrackToolUsageRequest.metadata:type_name -> aibridged.TrackToolUsageRequest.MetadataEntry - 11, // 3: aibridged.TrackTokenUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any - 11, // 4: aibridged.TrackUserPromptRequest.MetadataEntry.value:type_name -> google.protobuf.Any - 11, // 5: aibridged.TrackToolUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any - 0, // 6: aibridged.AIBridgeDaemon.StartSession:input_type -> aibridged.StartSessionRequest - 2, // 7: aibridged.AIBridgeDaemon.TrackTokenUsage:input_type -> aibridged.TrackTokenUsageRequest - 4, // 8: aibridged.AIBridgeDaemon.TrackUserPrompt:input_type -> aibridged.TrackUserPromptRequest - 6, // 9: aibridged.AIBridgeDaemon.TrackToolUsage:input_type -> aibridged.TrackToolUsageRequest - 1, // 10: aibridged.AIBridgeDaemon.StartSession:output_type -> aibridged.StartSessionResponse - 3, // 11: aibridged.AIBridgeDaemon.TrackTokenUsage:output_type -> aibridged.TrackTokenUsageResponse - 5, // 12: aibridged.AIBridgeDaemon.TrackUserPrompt:output_type -> aibridged.TrackUserPromptResponse - 7, // 13: aibridged.AIBridgeDaemon.TrackToolUsage:output_type -> aibridged.TrackToolUsageResponse - 10, // [10:14] is the sub-list for method output_type - 6, // [6:10] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name -} - -func init() { file_aibridged_proto_aibridged_proto_init() } -func file_aibridged_proto_aibridged_proto_init() { - if File_aibridged_proto_aibridged_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_aibridged_proto_aibridged_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StartSessionRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_aibridged_proto_aibridged_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StartSessionResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_aibridged_proto_aibridged_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackTokenUsageRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_aibridged_proto_aibridged_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackTokenUsageResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_aibridged_proto_aibridged_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackUserPromptRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_aibridged_proto_aibridged_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackUserPromptResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_aibridged_proto_aibridged_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackToolUsageRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_aibridged_proto_aibridged_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackToolUsageResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_aibridged_proto_aibridged_proto_rawDesc, - NumEnums: 0, - NumMessages: 11, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_aibridged_proto_aibridged_proto_goTypes, - DependencyIndexes: file_aibridged_proto_aibridged_proto_depIdxs, - MessageInfos: file_aibridged_proto_aibridged_proto_msgTypes, - }.Build() - File_aibridged_proto_aibridged_proto = out.File - file_aibridged_proto_aibridged_proto_rawDesc = nil - file_aibridged_proto_aibridged_proto_goTypes = nil - file_aibridged_proto_aibridged_proto_depIdxs = nil -} diff --git a/aibridged/proto/aibridged.proto b/aibridged/proto/aibridged.proto deleted file mode 100644 index 27351b5cd2672..0000000000000 --- a/aibridged/proto/aibridged.proto +++ /dev/null @@ -1,50 +0,0 @@ -syntax = "proto3"; -option go_package = "github.com/coder/coder/v2/aibridged/proto"; - -package aibridged; - -import "google/protobuf/any.proto"; - -message StartSessionRequest { - string session_id = 1; - string initiator_id = 2; - string provider = 3; - string model = 4; -} - -message StartSessionResponse {} - -message TrackTokenUsageRequest { - string session_id = 1; - string msg_id = 2; - int64 input_tokens = 3; - int64 output_tokens = 4; - map metadata = 5; -} -message TrackTokenUsageResponse {} - -message TrackUserPromptRequest { - string session_id = 1; - string msg_id = 2; - string prompt = 3; - map metadata = 4; -} -message TrackUserPromptResponse {} - -message TrackToolUsageRequest { - string session_id = 1; - string msg_id = 2; - string tool = 3; - string input = 4; - bool injected = 5; - map metadata = 6; -} -message TrackToolUsageResponse {} - -// AIBridgeDaemon describes the service exposed by coderd, which allows the AI bridge to send data to coderd. -service AIBridgeDaemon { - rpc StartSession(StartSessionRequest) returns (StartSessionResponse); - rpc TrackTokenUsage(TrackTokenUsageRequest) returns (TrackTokenUsageResponse); - rpc TrackUserPrompt(TrackUserPromptRequest) returns (TrackUserPromptResponse); - rpc TrackToolUsage(TrackToolUsageRequest) returns (TrackToolUsageResponse); -} diff --git a/aibridged/proto/aibridged_drpc.pb.go b/aibridged/proto/aibridged_drpc.pb.go deleted file mode 100644 index f04dfe4f1c05d..0000000000000 --- a/aibridged/proto/aibridged_drpc.pb.go +++ /dev/null @@ -1,231 +0,0 @@ -// Code generated by protoc-gen-go-drpc. DO NOT EDIT. -// protoc-gen-go-drpc version: v0.0.34 -// source: aibridged/proto/aibridged.proto - -package proto - -import ( - context "context" - errors "errors" - protojson "google.golang.org/protobuf/encoding/protojson" - proto "google.golang.org/protobuf/proto" - drpc "storj.io/drpc" - drpcerr "storj.io/drpc/drpcerr" -) - -type drpcEncoding_File_aibridged_proto_aibridged_proto struct{} - -func (drpcEncoding_File_aibridged_proto_aibridged_proto) Marshal(msg drpc.Message) ([]byte, error) { - return proto.Marshal(msg.(proto.Message)) -} - -func (drpcEncoding_File_aibridged_proto_aibridged_proto) MarshalAppend(buf []byte, msg drpc.Message) ([]byte, error) { - return proto.MarshalOptions{}.MarshalAppend(buf, msg.(proto.Message)) -} - -func (drpcEncoding_File_aibridged_proto_aibridged_proto) Unmarshal(buf []byte, msg drpc.Message) error { - return proto.Unmarshal(buf, msg.(proto.Message)) -} - -func (drpcEncoding_File_aibridged_proto_aibridged_proto) JSONMarshal(msg drpc.Message) ([]byte, error) { - return protojson.Marshal(msg.(proto.Message)) -} - -func (drpcEncoding_File_aibridged_proto_aibridged_proto) JSONUnmarshal(buf []byte, msg drpc.Message) error { - return protojson.Unmarshal(buf, msg.(proto.Message)) -} - -type DRPCAIBridgeDaemonClient interface { - DRPCConn() drpc.Conn - - StartSession(ctx context.Context, in *StartSessionRequest) (*StartSessionResponse, error) - TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) - TrackUserPrompt(ctx context.Context, in *TrackUserPromptRequest) (*TrackUserPromptResponse, error) - TrackToolUsage(ctx context.Context, in *TrackToolUsageRequest) (*TrackToolUsageResponse, error) -} - -type drpcAIBridgeDaemonClient struct { - cc drpc.Conn -} - -func NewDRPCAIBridgeDaemonClient(cc drpc.Conn) DRPCAIBridgeDaemonClient { - return &drpcAIBridgeDaemonClient{cc} -} - -func (c *drpcAIBridgeDaemonClient) DRPCConn() drpc.Conn { return c.cc } - -func (c *drpcAIBridgeDaemonClient) StartSession(ctx context.Context, in *StartSessionRequest) (*StartSessionResponse, error) { - out := new(StartSessionResponse) - err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/StartSession", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *drpcAIBridgeDaemonClient) TrackTokenUsage(ctx context.Context, in *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) { - out := new(TrackTokenUsageResponse) - err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackTokenUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *drpcAIBridgeDaemonClient) TrackUserPrompt(ctx context.Context, in *TrackUserPromptRequest) (*TrackUserPromptResponse, error) { - out := new(TrackUserPromptResponse) - err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackUserPrompt", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *drpcAIBridgeDaemonClient) TrackToolUsage(ctx context.Context, in *TrackToolUsageRequest) (*TrackToolUsageResponse, error) { - out := new(TrackToolUsageResponse) - err := c.cc.Invoke(ctx, "/aibridged.AIBridgeDaemon/TrackToolUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, in, out) - if err != nil { - return nil, err - } - return out, nil -} - -type DRPCAIBridgeDaemonServer interface { - StartSession(context.Context, *StartSessionRequest) (*StartSessionResponse, error) - TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) - TrackUserPrompt(context.Context, *TrackUserPromptRequest) (*TrackUserPromptResponse, error) - TrackToolUsage(context.Context, *TrackToolUsageRequest) (*TrackToolUsageResponse, error) -} - -type DRPCAIBridgeDaemonUnimplementedServer struct{} - -func (s *DRPCAIBridgeDaemonUnimplementedServer) StartSession(context.Context, *StartSessionRequest) (*StartSessionResponse, error) { - return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) -} - -func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackTokenUsage(context.Context, *TrackTokenUsageRequest) (*TrackTokenUsageResponse, error) { - return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) -} - -func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackUserPrompt(context.Context, *TrackUserPromptRequest) (*TrackUserPromptResponse, error) { - return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) -} - -func (s *DRPCAIBridgeDaemonUnimplementedServer) TrackToolUsage(context.Context, *TrackToolUsageRequest) (*TrackToolUsageResponse, error) { - return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) -} - -type DRPCAIBridgeDaemonDescription struct{} - -func (DRPCAIBridgeDaemonDescription) NumMethods() int { return 4 } - -func (DRPCAIBridgeDaemonDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { - switch n { - case 0: - return "/aibridged.AIBridgeDaemon/StartSession", drpcEncoding_File_aibridged_proto_aibridged_proto{}, - func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { - return srv.(DRPCAIBridgeDaemonServer). - StartSession( - ctx, - in1.(*StartSessionRequest), - ) - }, DRPCAIBridgeDaemonServer.StartSession, true - case 1: - return "/aibridged.AIBridgeDaemon/TrackTokenUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, - func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { - return srv.(DRPCAIBridgeDaemonServer). - TrackTokenUsage( - ctx, - in1.(*TrackTokenUsageRequest), - ) - }, DRPCAIBridgeDaemonServer.TrackTokenUsage, true - case 2: - return "/aibridged.AIBridgeDaemon/TrackUserPrompt", drpcEncoding_File_aibridged_proto_aibridged_proto{}, - func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { - return srv.(DRPCAIBridgeDaemonServer). - TrackUserPrompt( - ctx, - in1.(*TrackUserPromptRequest), - ) - }, DRPCAIBridgeDaemonServer.TrackUserPrompt, true - case 3: - return "/aibridged.AIBridgeDaemon/TrackToolUsage", drpcEncoding_File_aibridged_proto_aibridged_proto{}, - func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { - return srv.(DRPCAIBridgeDaemonServer). - TrackToolUsage( - ctx, - in1.(*TrackToolUsageRequest), - ) - }, DRPCAIBridgeDaemonServer.TrackToolUsage, true - default: - return "", nil, nil, nil, false - } -} - -func DRPCRegisterAIBridgeDaemon(mux drpc.Mux, impl DRPCAIBridgeDaemonServer) error { - return mux.Register(impl, DRPCAIBridgeDaemonDescription{}) -} - -type DRPCAIBridgeDaemon_StartSessionStream interface { - drpc.Stream - SendAndClose(*StartSessionResponse) error -} - -type drpcAIBridgeDaemon_StartSessionStream struct { - drpc.Stream -} - -func (x *drpcAIBridgeDaemon_StartSessionStream) SendAndClose(m *StartSessionResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { - return err - } - return x.CloseSend() -} - -type DRPCAIBridgeDaemon_TrackTokenUsageStream interface { - drpc.Stream - SendAndClose(*TrackTokenUsageResponse) error -} - -type drpcAIBridgeDaemon_TrackTokenUsageStream struct { - drpc.Stream -} - -func (x *drpcAIBridgeDaemon_TrackTokenUsageStream) SendAndClose(m *TrackTokenUsageResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { - return err - } - return x.CloseSend() -} - -type DRPCAIBridgeDaemon_TrackUserPromptStream interface { - drpc.Stream - SendAndClose(*TrackUserPromptResponse) error -} - -type drpcAIBridgeDaemon_TrackUserPromptStream struct { - drpc.Stream -} - -func (x *drpcAIBridgeDaemon_TrackUserPromptStream) SendAndClose(m *TrackUserPromptResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { - return err - } - return x.CloseSend() -} - -type DRPCAIBridgeDaemon_TrackToolUsageStream interface { - drpc.Stream - SendAndClose(*TrackToolUsageResponse) error -} - -type drpcAIBridgeDaemon_TrackToolUsageStream struct { - drpc.Stream -} - -func (x *drpcAIBridgeDaemon_TrackToolUsageStream) SendAndClose(m *TrackToolUsageResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_aibridged_proto_aibridged_proto{}); err != nil { - return err - } - return x.CloseSend() -} diff --git a/aibridged/provider.go b/aibridged/provider.go deleted file mode 100644 index 7666846070117..0000000000000 --- a/aibridged/provider.go +++ /dev/null @@ -1,14 +0,0 @@ -package aibridged - -import ( - "net/http" -) - -type Provider interface { - CreateSession(w http.ResponseWriter, r *http.Request, tools ToolRegistry) (Session, error) - Identifier() string - BaseURL() string - Key() string -} - -type ProviderRegistry map[string]Provider diff --git a/aibridged/provider_anthropic.go b/aibridged/provider_anthropic.go deleted file mode 100644 index 1f8aef02f3558..0000000000000 --- a/aibridged/provider_anthropic.go +++ /dev/null @@ -1,76 +0,0 @@ -package aibridged - -import ( - "encoding/json" - "io" - "net/http" - "os" - - "github.com/anthropics/anthropic-sdk-go" - "github.com/anthropics/anthropic-sdk-go/option" - "golang.org/x/xerrors" -) - -var _ Provider = &AnthropicMessagesProvider{} - -// AnthropicMessagesProvider allows for interactions with the Anthropic Messages API. -// See https://docs.anthropic.com/en/api/messages -type AnthropicMessagesProvider struct { - baseURL, key string -} - -func NewAnthropicMessagesProvider(baseURL, key string) *AnthropicMessagesProvider { - if baseURL == "" { - baseURL = "https://api.anthropic.com/" - } - if key == "" { - key = os.Getenv("ANTHROPIC_API_KEY") - } - - return &AnthropicMessagesProvider{ - baseURL: baseURL, - key: key, - } -} - -func (p *AnthropicMessagesProvider) CreateSession(w http.ResponseWriter, r *http.Request, tools ToolRegistry) (Session, error) { - payload, err := io.ReadAll(r.Body) - if err != nil { - return nil, xerrors.Errorf("read body: %w", err) - } - - switch r.URL.Path { - case "/anthropic/v1/messages": - var req BetaMessageNewParamsWrapper - if err := json.Unmarshal(payload, &req); err != nil { - return nil, xerrors.Errorf("failed to unmarshal request: %w", err) - } - - if req.Stream { - return NewAnthropicMessagesStreamingSession(&req, p.baseURL, p.key), nil - } - - return NewAnthropicMessagesBlockingSession(&req, p.baseURL, p.key), nil - } - - return nil, UnknownRoute -} - -func (p *AnthropicMessagesProvider) Identifier() string { - return ProviderAnthropic -} - -func (p *AnthropicMessagesProvider) BaseURL() string { - return p.baseURL -} - -func (p *AnthropicMessagesProvider) Key() string { - return p.key -} - -func newAnthropicClient(baseURL, key string, opts ...option.RequestOption) anthropic.Client { - opts = append(opts, option.WithAPIKey(key)) - opts = append(opts, option.WithBaseURL(baseURL)) - - return anthropic.NewClient(opts...) -} diff --git a/aibridged/provider_openai.go b/aibridged/provider_openai.go deleted file mode 100644 index 21abd24e0dcb0..0000000000000 --- a/aibridged/provider_openai.go +++ /dev/null @@ -1,78 +0,0 @@ -package aibridged - -import ( - "encoding/json" - "io" - "net/http" - "os" - - "github.com/openai/openai-go" - "github.com/openai/openai-go/option" - "golang.org/x/xerrors" -) - -var _ Provider = &OpenAIProvider{} - -// OpenAIProvider allows for interactions with the Chat Completions API. -// See https://platform.openai.com/docs/api-reference/chat. -type OpenAIProvider struct { - baseURL, key string -} - -func NewOpenAIProvider(baseURL, key string) *OpenAIProvider { - if baseURL == "" { - baseURL = "https://api.openai.com/v1/" - } - - if key == "" { - key = os.Getenv("OPENAI_API_KEY") - } - - return &OpenAIProvider{ - baseURL: baseURL, - key: key, - } -} - -func (p *OpenAIProvider) Identifier() string { - return ProviderOpenAI -} - -func (p *OpenAIProvider) CreateSession(w http.ResponseWriter, r *http.Request, tools ToolRegistry) (Session, error) { - payload, err := io.ReadAll(r.Body) - if err != nil { - return nil, xerrors.Errorf("read body: %w", err) - } - - switch r.URL.Path { - case "/openai/v1/chat/completions": - var req ChatCompletionNewParamsWrapper - if err := json.Unmarshal(payload, &req); err != nil { - return nil, xerrors.Errorf("unmarshal request body: %w", err) - } - - if req.Stream { - return NewOpenAIStreamingChatSession(&req, p.baseURL, p.key), nil - } else { - return NewOpenAIBlockingChatSession(&req, p.baseURL, p.key), nil - } - } - - return nil, UnknownRoute -} - -func (p *OpenAIProvider) BaseURL() string { - return p.baseURL -} - -func (p *OpenAIProvider) Key() string { - return p.key -} - -func newOpenAIClient(baseURL, key string) openai.Client { - var opts []option.RequestOption - opts = append(opts, option.WithAPIKey(key)) - opts = append(opts, option.WithBaseURL(baseURL)) - - return openai.NewClient(opts...) -} diff --git a/aibridged/session.go b/aibridged/session.go deleted file mode 100644 index 6869fe0c4f9bf..0000000000000 --- a/aibridged/session.go +++ /dev/null @@ -1,61 +0,0 @@ -package aibridged - -import ( - "context" - "fmt" - "net/http" - - "github.com/google/uuid" - "golang.org/x/xerrors" - - "cdr.dev/slog" - "github.com/coder/coder/v2/aibridged/proto" -) - -// Session describes a (potentially) stateful interaction with an AI provider. -type Session interface { - Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string - Model() string - ProcessRequest(w http.ResponseWriter, r *http.Request) error -} - -var UnknownRoute = xerrors.New("unknown route") - -func NewSessionProcessor(p Provider, logger slog.Logger, client proto.DRPCAIBridgeDaemonClient, tools ToolRegistry) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - sess, err := p.CreateSession(w, r, tools) - if err != nil { - logger.Error(r.Context(), "failed to create session", slog.Error(err), slog.F("path", r.URL.Path)) - http.Error(w, fmt.Sprintf("failed to create %q session", r.URL.Path), http.StatusInternalServerError) - return - } - - sessID := sess.Init(logger, NewDRPCTracker(client), NewInjectedToolManager(tools)) - logger = logger.With(slog.F("route", r.URL.Path), slog.F("provider", p.Identifier()), slog.F("session_id", sessID)) - - userID, ok := r.Context().Value(ContextKeyBridgeUserID{}).(uuid.UUID) - if !ok { - logger.Error(r.Context(), "missing initiator ID in context") - http.Error(w, "unable to retrieve initiator", http.StatusInternalServerError) - return - } - - _, err = client.StartSession(r.Context(), &proto.StartSessionRequest{ - SessionId: sessID, - InitiatorId: userID.String(), - Provider: p.Identifier(), - Model: sess.Model(), - }) - if err != nil { - logger.Error(r.Context(), "failed to start session", slog.Error(err)) - http.Error(w, "failed to start session", http.StatusInternalServerError) - return - } - - logger.Debug(context.Background(), "started session") - - if err := sess.ProcessRequest(w, r); err != nil { - logger.Error(r.Context(), "session execution failed", slog.Error(err)) - } - } -} diff --git a/aibridged/session_anthropic_messages_base.go b/aibridged/session_anthropic_messages_base.go deleted file mode 100644 index 3b7edc6191e81..0000000000000 --- a/aibridged/session_anthropic_messages_base.go +++ /dev/null @@ -1,77 +0,0 @@ -package aibridged - -import ( - "strings" - - "github.com/anthropics/anthropic-sdk-go" - "github.com/google/uuid" - - "cdr.dev/slog" -) - -type AnthropicMessagesSessionBase struct { - id string - - req *BetaMessageNewParamsWrapper - - baseURL, key string - logger slog.Logger - - tracker Tracker - toolMgr ToolManager -} - -func (s *AnthropicMessagesSessionBase) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { - s.id = uuid.NewString() - - s.logger = logger.With(slog.F("session_id", s.id)) - - s.tracker = tracker - s.toolMgr = toolMgr - - return s.id -} - -func (s *AnthropicMessagesSessionBase) Model() string { - if s.req == nil { - return "?" - } - - return string(s.req.Model) -} - -func (s *AnthropicMessagesSessionBase) injectTools() { - if s.req == nil { - return - } - - // Inject tools. - for _, tool := range s.toolMgr.ListTools() { - s.req.Tools = append(s.req.Tools, anthropic.BetaToolUnionParam{ - OfTool: &anthropic.BetaToolParam{ - InputSchema: anthropic.BetaToolInputSchemaParam{ - Properties: tool.Params, - Required: tool.Required, - }, - Name: tool.ID, - Description: anthropic.String(tool.Description), - Type: anthropic.BetaToolTypeCustom, - }, - }) - } - - // Note: Parallel tool calls are disabled to avoid tool_use/tool_result block mismatches. - s.req.ToolChoice = anthropic.BetaToolChoiceUnionParam{ - OfAny: &anthropic.BetaToolChoiceAnyParam{ - Type: "auto", - DisableParallelToolUse: anthropic.Bool(true), - }, - } -} - -// isSmallFastModel checks if the model is a small/fast model (Haiku 3.5). -// These models are optimized for tasks like code autocomplete and other small, quick operations. -// See `ANTHROPIC_SMALL_FAST_MODEL`: https://docs.anthropic.com/en/docs/claude-code/settings#environment-variables -func (s *AnthropicMessagesSessionBase) isSmallFastModel() bool { - return strings.Contains(string(s.req.Model), "3-5-haiku") -} diff --git a/aibridged/session_anthropic_messages_blocking.go b/aibridged/session_anthropic_messages_blocking.go deleted file mode 100644 index 669b96d50bd41..0000000000000 --- a/aibridged/session_anthropic_messages_blocking.go +++ /dev/null @@ -1,280 +0,0 @@ -package aibridged - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - "time" - - "github.com/anthropics/anthropic-sdk-go" - "github.com/anthropics/anthropic-sdk-go/option" - "github.com/mark3labs/mcp-go/mcp" - "golang.org/x/xerrors" - - "cdr.dev/slog" -) - -var _ Session = &AnthropicMessagesBlockingSession{} - -type AnthropicMessagesBlockingSession struct { - AnthropicMessagesSessionBase -} - -func NewAnthropicMessagesBlockingSession(req *BetaMessageNewParamsWrapper, baseURL, key string) *AnthropicMessagesBlockingSession { - return &AnthropicMessagesBlockingSession{AnthropicMessagesSessionBase: AnthropicMessagesSessionBase{ - req: req, - baseURL: baseURL, - key: key, - }} -} - -func (s *AnthropicMessagesBlockingSession) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { - return s.AnthropicMessagesSessionBase.Init(logger.Named("blocking"), tracker, toolMgr) -} - -func (s *AnthropicMessagesBlockingSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { - if s.req == nil { - return xerrors.Errorf("developer error: req is nil") - } - - ctx := r.Context() - - s.injectTools() - - var ( - prompt *string - err error - ) - // Track user prompt if not a small/fast model - if !s.isSmallFastModel() { - prompt, err = s.req.LastUserPrompt() - if err != nil { - s.logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) - } - } - - // Add beta header if present in the request. - var opts []option.RequestOption - if reqBetaHeader := r.Header.Get("anthropic-beta"); strings.TrimSpace(reqBetaHeader) != "" { - opts = append(opts, option.WithHeader("anthropic-beta", reqBetaHeader)) - } - opts = append(opts, option.WithRequestTimeout(time.Second*30)) - - client := newAnthropicClient(s.baseURL, s.key, opts...) // TODO: configurable timeout - messages := s.req.BetaMessageNewParams - logger := s.logger.With(slog.F("model", s.req.Model)) - - var resp *anthropic.BetaMessage - - for { - resp, err = client.Beta.Messages.New(ctx, messages) - if err != nil { - if isConnectionError(err) { - logger.Warn(ctx, "upstream connection closed", slog.Error(err)) - return xerrors.Errorf("upstream connection closed: %w", err) - } - - logger.Error(ctx, "anthropic API error", slog.Error(err)) - if antErr := getAnthropicErrorResponse(err); antErr != nil { - http.Error(w, antErr.Error.Message, antErr.StatusCode) - return xerrors.Errorf("api error: %w", err) - } - - logger.Error(ctx, "upstream API error", slog.Error(err)) - http.Error(w, "internal error", http.StatusInternalServerError) - return xerrors.Errorf("upstream API error: %w", err) - } - - if prompt != nil { - if err := s.tracker.TrackPromptUsage(ctx, s.id, resp.ID, *prompt, nil); err != nil { - s.logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) - } - } - - if err := s.tracker.TrackTokensUsage(ctx, s.id, resp.ID, resp.Usage.InputTokens, resp.Usage.OutputTokens, Metadata{ - "web_search_requests": resp.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": resp.Usage.CacheCreationInputTokens, - "cache_read_input": resp.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": resp.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": resp.Usage.CacheCreation.Ephemeral5mInputTokens, - }); err != nil { - logger.Warn(ctx, "failed to track token usage", slog.Error(err)) - } - - // Handle tool calls for non-streaming. - var pendingToolCalls []anthropic.BetaToolUseBlock - for _, c := range resp.Content { - toolUse := c.AsToolUse() - if toolUse.ID == "" { - continue - } - - if s.toolMgr.GetTool(toolUse.Name) != nil { - pendingToolCalls = append(pendingToolCalls, toolUse) - continue - } - - // If tool is not injected, track it since the client will be handling it. - if err := s.tracker.TrackToolUsage(ctx, s.id, resp.ID, toolUse.Name, toolUse.Input, false, nil); err != nil { - logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) - } - } - - // If no injected tool calls, we're done. - if len(pendingToolCalls) == 0 { - break - } - - // Append the assistant's message (which contains the tool_use block) - // to the messages for the next API call. - messages.Messages = append(messages.Messages, resp.ToParam()) - - // Process each pending tool call. - for _, tc := range pendingToolCalls { - tool := s.toolMgr.GetTool(tc.Name) - if tool == nil { - logger.Warn(ctx, "tool not found in manager", slog.F("tool", tc.Name)) - // Continue to next tool call, but still append an error tool_result - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error: tool %s not found", tc.Name), true)), - ) - continue - } - - var args map[string]any - serialized, err := json.Marshal(tc.Input) - if err != nil { - logger.Warn(ctx, "failed to marshal tool args for unmarshal", slog.Error(err), slog.F("tool", tc.Name)) - // Continue to next tool call, but still append an error tool_result - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error unmarshaling tool arguments: %v", err), true)), - ) - continue - } else if err := json.Unmarshal(serialized, &args); err != nil { - logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", tc.Name)) - // Continue to next tool call, but still append an error tool_result - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error unmarshaling tool arguments: %v", err), true)), - ) - continue - } - - // Track injected tool usage - strip MCP tool namespacing if possible - toolName := tc.Name - if _, tool, err := DecodeToolID(toolName); err == nil { - toolName = tool - } - if err := s.tracker.TrackToolUsage(ctx, s.id, resp.ID, toolName, args, true, nil); err != nil { - logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) - } - - res, err := tool.Call(ctx, args) - if err != nil { - // Always provide a tool_result even if the tool call failed - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(tc.ID, fmt.Sprintf("Error calling tool: %v", err), true)), - ) - continue - } - - // Process tool result - toolResult := anthropic.BetaContentBlockParamUnion{ - OfToolResult: &anthropic.BetaToolResultBlockParam{ - ToolUseID: tc.ID, - IsError: anthropic.Bool(false), - }, - } - - var hasValidResult bool - for _, content := range res.Content { - switch cb := content.(type) { - case mcp.TextContent: - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: cb.Text, - }, - }) - hasValidResult = true - case mcp.EmbeddedResource: - switch resource := cb.Resource.(type) { - case mcp.TextResourceContents: - val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", - resource.MIMEType, resource.URI, resource.Text) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: val, - }, - }) - hasValidResult = true - case mcp.BlobResourceContents: - val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", - resource.MIMEType, resource.URI, resource.Blob) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: val, - }, - }) - hasValidResult = true - default: - s.logger.Error(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource))) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: unknown embedded resource type", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - hasValidResult = true - } - default: - s.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb))) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: unsupported tool result type", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - hasValidResult = true - } - } - - // If no content was processed, still add a tool_result - if !hasValidResult { - s.logger.Error(ctx, "no tool result added", slog.F("content_len", len(res.Content)), slog.F("is_error", res.IsError)) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: no valid tool result content", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - } - - if len(toolResult.OfToolResult.Content) > 0 { - messages.Messages = append(messages.Messages, anthropic.NewBetaUserMessage(toolResult)) - } - } - } - - if resp == nil { - return nil - } - - // Overwrite response identifier since proxy obscures injected tool call invocations. - resp.ID = s.id - - out, err := json.Marshal(resp) - if err != nil { - http.Error(w, "error marshaling response", http.StatusInternalServerError) - return xerrors.Errorf("failed to marshal response: %w", err) - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(out) - - return nil -} - -func (s *AnthropicMessagesBlockingSession) Close() error { - return nil -} diff --git a/aibridged/session_anthropic_messages_streaming.go b/aibridged/session_anthropic_messages_streaming.go deleted file mode 100644 index 1b8dbfba8c940..0000000000000 --- a/aibridged/session_anthropic_messages_streaming.go +++ /dev/null @@ -1,378 +0,0 @@ -package aibridged - -import ( - "context" - "fmt" - "net/http" - "strings" - "sync" - - "github.com/anthropics/anthropic-sdk-go" - "github.com/anthropics/anthropic-sdk-go/option" - ant_constant "github.com/anthropics/anthropic-sdk-go/shared/constant" - "github.com/mark3labs/mcp-go/mcp" - "github.com/openai/openai-go/shared/constant" - "golang.org/x/xerrors" - - "cdr.dev/slog" -) - -var _ Session = &AnthropicMessagesStreamingSession{} - -type AnthropicMessagesStreamingSession struct { - AnthropicMessagesSessionBase -} - -func NewAnthropicMessagesStreamingSession(req *BetaMessageNewParamsWrapper, baseURL, key string) *AnthropicMessagesStreamingSession { - return &AnthropicMessagesStreamingSession{AnthropicMessagesSessionBase: AnthropicMessagesSessionBase{ - req: req, - baseURL: baseURL, - key: key, - }} -} - -func (s *AnthropicMessagesStreamingSession) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { - return s.AnthropicMessagesSessionBase.Init(logger.Named("streaming"), tracker, toolMgr) -} - -func (s *AnthropicMessagesStreamingSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { - if s.req == nil { - return xerrors.Errorf("developer error: req is nil") - } - - // Allow us to interrupt watch via cancel. - ctx, cancel := context.WithCancel(r.Context()) - defer cancel() - r = r.WithContext(ctx) // Rewire context for SSE cancellation. - - s.injectTools() - - // Track user prompt if not a small/fast model - if !s.isSmallFastModel() { - prompt, err := s.req.LastUserPrompt() - if err != nil { - s.logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) - } else if prompt != nil { - if err := s.tracker.TrackPromptUsage(ctx, s.id, "", *prompt, nil); err != nil { - s.logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) - } - } - } - - streamCtx, streamCancel := context.WithCancelCause(ctx) - defer streamCancel(xerrors.New("deferred")) - - es := newEventStream(anthropicEventStream) - - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - defer func() { - if err := es.Close(streamCtx); err != nil { - s.logger.Error(ctx, "error closing stream", slog.Error(err)) - } - }() - - BasicSSESender(streamCtx, es, s.logger.Named("sse-sender")).ServeHTTP(w, r) - }() - - // Add beta header if present in the request. - var opts []option.RequestOption - if reqBetaHeader := r.Header.Get("anthropic-beta"); strings.TrimSpace(reqBetaHeader) != "" { - opts = append(opts, option.WithHeader("anthropic-beta", reqBetaHeader)) - } - - client := newAnthropicClient(s.baseURL, s.key, opts...) - messages := s.req.BetaMessageNewParams - logger := s.logger.With(slog.F("model", s.req.Model)) - - // Accumulate usage across the entire streaming interaction (including tool reinvocations). - var cumulativeInputTokens int64 - var cumulativeOutputTokens int64 - - isFirst := true - for { - newStream: - stream := client.Beta.Messages.NewStreaming(streamCtx, messages) - - var message anthropic.BetaMessage - var lastToolName string - - pendingToolCalls := make(map[string]string) - - for stream.Next() { - event := stream.Current() - if err := message.Accumulate(event); err != nil { - logger.Error(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON())) - http.Error(w, "failed to proxy request", http.StatusInternalServerError) - return xerrors.Errorf("failed to accumulate streaming events: %w", err) - } - - // Tool-related handling. - switch event.Type { - case string(constant.ValueOf[ant_constant.ContentBlockStart]()): - switch block := event.AsContentBlockStart().ContentBlock.AsAny().(type) { - case anthropic.BetaToolUseBlock: - lastToolName = block.Name - - if s.toolMgr.GetTool(block.Name) != nil { - pendingToolCalls[block.Name] = block.ID - // Don't relay this event back, otherwise the client will try invoke the tool as well. - continue - } - } - case string(constant.ValueOf[ant_constant.ContentBlockDelta]()): - if len(pendingToolCalls) > 0 && s.toolMgr.GetTool(lastToolName) != nil { - // We're busy with a tool call, don't relay this event back. - continue - } - case string(constant.ValueOf[ant_constant.ContentBlockStop]()): - // Reset the tool name - isInjected := s.toolMgr.GetTool(lastToolName) != nil - lastToolName = "" - - if len(pendingToolCalls) > 0 && isInjected { - // We're busy with a tool call, don't relay this event back. - continue - } - case string(ant_constant.ValueOf[ant_constant.MessageStart]()): - start := event.AsMessageStart() - cumulativeInputTokens += start.Message.Usage.InputTokens - cumulativeOutputTokens += start.Message.Usage.OutputTokens - if !isFirst { - // Don't send message_start unless first message! - // We're sending multiple messages back and forth with the API, but from the client's perspective - // they're just expecting a single message. - continue - } - case string(ant_constant.ValueOf[ant_constant.MessageDelta]()): - delta := event.AsMessageDelta() - cumulativeInputTokens += delta.Usage.InputTokens - cumulativeOutputTokens += delta.Usage.OutputTokens - // Don't relay message_delta events which indicate injected tool use. - if len(pendingToolCalls) > 0 && s.toolMgr.GetTool(lastToolName) != nil { - continue - } - - // If currently calling a tool. - if len(message.Content) > 0 && message.Content[len(message.Content)-1].Type == string(ant_constant.ValueOf[ant_constant.ToolUse]()) { - toolName := message.Content[len(message.Content)-1].AsToolUse().Name - if len(pendingToolCalls) > 0 && s.toolMgr.GetTool(toolName) != nil { - continue - } - } - - // Don't send message_stop until all tools have been called. - case string(ant_constant.ValueOf[ant_constant.MessageStop]()): - if len(pendingToolCalls) > 0 { - // Append the whole message from this stream as context since we'll be sending a new request with the tool results. - messages.Messages = append(messages.Messages, message.ToParam()) - - for name, id := range pendingToolCalls { - if s.toolMgr.GetTool(name) == nil { - // Not an MCP proxy call, don't do anything. - continue - } - - tool := s.toolMgr.GetTool(name) - if tool == nil { - logger.Error(ctx, "tool not found in manager", slog.F("tool_name", name)) - continue - } - - var ( - input any - foundTool bool - foundTools int - ) - for _, block := range message.Content { - switch variant := block.AsAny().(type) { - case anthropic.BetaToolUseBlock: - foundTools++ - if variant.Name == name { - input = variant.Input - foundTool = true - } - } - } - - if !foundTool { - logger.Error(ctx, "failed to find tool input", slog.F("tool_name", name), slog.F("found_tools", foundTools)) - continue - } - - // Track injected tool usage - strip MCP tool namespacing if possible - toolName := tool.Name - if _, decodedTool, err := DecodeToolID(toolName); err == nil { - toolName = decodedTool - } - if err := s.tracker.TrackToolUsage(streamCtx, s.id, message.ID, toolName, input, true, nil); err != nil { - logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) - } - - res, err := tool.Call(streamCtx, input) - if err != nil { - // Always provide a tool_result even if the tool call failed - messages.Messages = append(messages.Messages, - anthropic.NewBetaUserMessage(anthropic.NewBetaToolResultBlock(id, fmt.Sprintf("Error calling tool: %v", err), true)), - ) - continue - } - - // Process tool result - toolResult := anthropic.BetaContentBlockParamUnion{ - OfToolResult: &anthropic.BetaToolResultBlockParam{ - ToolUseID: id, - IsError: anthropic.Bool(false), - }, - } - - var hasValidResult bool - for _, content := range res.Content { - switch cb := content.(type) { - case mcp.TextContent: - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: cb.Text, - }, - }) - hasValidResult = true - case mcp.EmbeddedResource: - switch resource := cb.Resource.(type) { - case mcp.TextResourceContents: - val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", - resource.MIMEType, resource.URI, resource.Text) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: val, - }, - }) - hasValidResult = true - case mcp.BlobResourceContents: - val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s", - resource.MIMEType, resource.URI, resource.Blob) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: val, - }, - }) - hasValidResult = true - default: - s.logger.Error(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource))) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: unknown embedded resource type", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - hasValidResult = true - } - default: - s.logger.Error(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb))) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: unsupported tool result type", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - hasValidResult = true - } - } - - // If no content was processed, still add a tool_result - if !hasValidResult { - s.logger.Error(ctx, "no tool result added", slog.F("content_len", len(res.Content)), slog.F("is_error", res.IsError)) - toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.BetaToolResultBlockParamContentUnion{ - OfText: &anthropic.BetaTextBlockParam{ - Text: "Error: no valid tool result content", - }, - }) - toolResult.OfToolResult.IsError = anthropic.Bool(true) - } - - if len(toolResult.OfToolResult.Content) > 0 { - messages.Messages = append(messages.Messages, anthropic.NewBetaUserMessage(toolResult)) - } - } - - // Causes a new stream to be run with updated messages. - isFirst = false - goto newStream - } else { - // Find all the non-injected tools and track their uses. - for _, block := range message.Content { - switch variant := block.AsAny().(type) { - case anthropic.BetaToolUseBlock: - if s.toolMgr.GetTool(variant.Name) != nil { - continue - } - - if err := s.tracker.TrackToolUsage(streamCtx, s.id, message.ID, variant.Name, variant.Input, false, nil); err != nil { - logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) - } - } - } - } - } - - // Overwrite response identifier since proxy obscures injected tool call invocations. - event.Message.ID = s.id - if err := es.TrySend(streamCtx, event); err != nil { - if isConnectionError(err) { - s.logger.Debug(ctx, "client disconnected during sending event", slog.Error(err)) - return nil // Stop processing if client disconnected - } else { - s.logger.Error(ctx, "error during sending event", slog.Error(err)) - } - } - } - - // Emit a single, final token usage total for this stream. - metadata := Metadata{ - "web_search_requests": message.Usage.ServerToolUse.WebSearchRequests, - "cache_creation_input": message.Usage.CacheCreationInputTokens, - "cache_read_input": message.Usage.CacheReadInputTokens, - "cache_ephemeral_1h_input": message.Usage.CacheCreation.Ephemeral1hInputTokens, - "cache_ephemeral_5m_input": message.Usage.CacheCreation.Ephemeral5mInputTokens, - } - if err := s.tracker.TrackTokensUsage(streamCtx, s.id, message.ID, cumulativeInputTokens, cumulativeOutputTokens, metadata); err != nil { - logger.Warn(ctx, "failed to track token usage", slog.Error(err)) - } - - var streamErr error - if streamErr = stream.Err(); streamErr != nil { - if isConnectionError(streamErr) { - logger.Warn(ctx, "upstream connection closed", slog.Error(streamErr)) - } else { - logger.Error(ctx, "anthropic stream error", slog.Error(streamErr)) - if antErr := getAnthropicErrorResponse(streamErr); antErr != nil { - if err := es.TrySend(streamCtx, antErr); err != nil { - logger.Error(ctx, "failed to send error", slog.Error(err)) - } - } - } - } - - if err := es.Close(streamCtx); err != nil { - logger.Error(ctx, "failed to close event stream", slog.Error(err)) - } - - wg.Wait() - - // Ensure we flush all the remaining data before ending. - flush(w) - - if streamErr != nil { - streamCancel(xerrors.Errorf("stream err: %w", streamErr)) - } else { - streamCancel(xerrors.New("gracefully done")) - } - - <-streamCtx.Done() - break - } - - return nil -} diff --git a/aibridged/session_openai_chat_base.go b/aibridged/session_openai_chat_base.go deleted file mode 100644 index ab6edb7c6f8c1..0000000000000 --- a/aibridged/session_openai_chat_base.go +++ /dev/null @@ -1,76 +0,0 @@ -package aibridged - -import ( - "github.com/google/uuid" - "github.com/openai/openai-go" - - "cdr.dev/slog" -) - -type OpenAIChatSessionBase struct { - id string - - req *ChatCompletionNewParamsWrapper - - baseURL, key string - logger slog.Logger - - tracker Tracker - toolMgr ToolManager -} - -func (s *OpenAIChatSessionBase) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { - s.id = uuid.NewString() - - s.logger = logger.With(slog.F("session_id", s.id)) - - s.tracker = tracker - s.toolMgr = toolMgr - - return s.id -} - -func (s *OpenAIChatSessionBase) Model() string { - if s.req == nil { - return "?" - } - - return string(s.req.Model) -} - -func (s *OpenAIChatSessionBase) newErrorResponse(err error) map[string]interface{} { - return map[string]interface{}{ - "error": true, - "message": err.Error(), - } -} - -func (s *OpenAIChatSessionBase) injectTools() { - if s.req == nil { - return - } - - // Inject tools. - for _, tool := range s.toolMgr.ListTools() { - fn := openai.ChatCompletionToolParam{ - Function: openai.FunctionDefinitionParam{ - Name: tool.ID, - Strict: openai.Bool(false), // TODO: configurable. - Description: openai.String(tool.Description), - Parameters: openai.FunctionParameters{ - "type": "object", - "properties": tool.Params, - // "additionalProperties": false, // Only relevant when strict=true. - }, - }, - } - - // Otherwise the request fails with "None is not of type 'array'" if a nil slice is given. - if len(tool.Required) > 0 { - // Must list ALL properties when strict=true. - fn.Function.Parameters["required"] = tool.Required - } - - s.req.Tools = append(s.req.Tools, fn) - } -} diff --git a/aibridged/session_openai_chat_blocking.go b/aibridged/session_openai_chat_blocking.go deleted file mode 100644 index efb4d3ac5ee83..0000000000000 --- a/aibridged/session_openai_chat_blocking.go +++ /dev/null @@ -1,199 +0,0 @@ -package aibridged - -import ( - "bytes" - "encoding/json" - "errors" - "net/http" - "strings" - - "github.com/openai/openai-go" - "golang.org/x/xerrors" - - "cdr.dev/slog" -) - -var _ Session = &OpenAIBlockingChatSession{} - -type OpenAIBlockingChatSession struct { - OpenAIChatSessionBase -} - -func NewOpenAIBlockingChatSession(req *ChatCompletionNewParamsWrapper, baseURL, key string) *OpenAIBlockingChatSession { - return &OpenAIBlockingChatSession{OpenAIChatSessionBase: OpenAIChatSessionBase{ - req: req, - baseURL: baseURL, - key: key, - }} -} - -func (s *OpenAIBlockingChatSession) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { - return s.OpenAIChatSessionBase.Init(logger.Named("blocking"), tracker, toolMgr) -} - -func (s *OpenAIBlockingChatSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { - if s.req == nil { - return xerrors.Errorf("developer error: req is nil") - } - - ctx := r.Context() - client := newOpenAIClient(s.baseURL, s.key) - logger := s.logger.With(slog.F("model", s.req.Model)) - - var cumulativeUsage openai.CompletionUsage - var ( - completion *openai.ChatCompletion - err error - ) - - s.injectTools() - - prompt, err := s.req.LastUserPrompt() - if err != nil { - logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) - } - - for { - completion, err = client.Chat.Completions.New(ctx, s.req.ChatCompletionNewParams) - if err != nil { - break - } - - if prompt != nil { - if err := s.tracker.TrackPromptUsage(ctx, s.id, completion.ID, *prompt, nil); err != nil { - logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) - } - } - - // Track cumulative usage - cumulativeUsage = sumUsage(cumulativeUsage, completion.Usage) - - // Track token usage - if err := s.tracker.TrackTokensUsage(ctx, s.id, completion.ID, cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ - "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, - "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, - "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, - "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, - "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, - "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, - }); err != nil { - logger.Warn(ctx, "failed to track tokens usage", slog.Error(err)) - } - - // Check if we have tool calls to process - var pendingToolCalls []openai.ChatCompletionMessageToolCall - if len(completion.Choices) > 0 && completion.Choices[0].Message.ToolCalls != nil { - for _, toolCall := range completion.Choices[0].Message.ToolCalls { - if s.toolMgr.GetTool(toolCall.Function.Name) != nil { - pendingToolCalls = append(pendingToolCalls, toolCall) - } else { - if err := s.tracker.TrackToolUsage(ctx, s.id, completion.ID, toolCall.Function.Name, toolCall.Function.Arguments, false, nil); err != nil { - s.logger.Warn(ctx, "failed to track tool usage", slog.Error(err), slog.F("tool", toolCall.Function.Name)) - } - } - } - } - - // If no injected tool calls, we're done - if len(pendingToolCalls) == 0 { - break - } - - appendedPrevMsg := false - // Process each pending tool call - for _, tc := range pendingToolCalls { - tool := s.toolMgr.GetTool(tc.Function.Name) - if tool == nil { - // Not a known tool, don't do anything. - logger.Warn(ctx, "pending tool call for non-managed tool, skipping", slog.F("tool", tc.Function.Name)) - continue - } - // Only do this once. - if !appendedPrevMsg { - // Append the whole message from this stream as context since we'll be sending a new request with the tool results. - s.req.Messages = append(s.req.Messages, completion.Choices[0].Message.ToParam()) - appendedPrevMsg = true - } - - if err := s.tracker.TrackToolUsage(ctx, s.id, completion.ID, tool.Name, tc.Function.Arguments, true, nil); err != nil { - logger.Warn(ctx, "failed to track tool usage", slog.Error(err), slog.F("tool", tool.Name)) - } - - var ( - args map[string]string - buf bytes.Buffer - ) - _ = json.NewEncoder(&buf).Encode(tc.Function.Arguments) - _ = json.NewDecoder(&buf).Decode(&args) - res, err := tool.Call(ctx, args) - if err != nil { - // Always provide a tool result even if the tool call failed - errorResponse := map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - } - errorJSON, _ := json.Marshal(errorResponse) - s.req.Messages = append(s.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - var out strings.Builder - if err := json.NewEncoder(&out).Encode(res); err != nil { - logger.Error(ctx, "failed to encode tool response", slog.Error(err)) - // Always provide a tool result even if encoding failed - errorResponse := map[string]interface{}{ - // TODO: session ID? - "error": true, - "message": err.Error(), - } - errorJSON, _ := json.Marshal(errorResponse) - s.req.Messages = append(s.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - s.req.Messages = append(s.req.Messages, openai.ToolMessage(out.String(), tc.ID)) - } - } - - // TODO: these probably have to be formatted as JSON errs? - if err != nil { - if isConnectionError(err) { - http.Error(w, err.Error(), http.StatusInternalServerError) - return xerrors.Errorf("upstream connection closed: %w", err) - } - var apierr *openai.Error - if errors.As(err, &apierr) { - http.Error(w, apierr.Message, apierr.StatusCode) - return xerrors.Errorf("api error: %w", apierr) - } - - http.Error(w, err.Error(), http.StatusInternalServerError) - return xerrors.Errorf("chat completion failed: %w", err) - } - - if completion == nil { - return nil - } - - // Overwrite response identifier since proxy obscures injected tool call invocations. - completion.ID = s.id - - // Update the cumulative usage in the final response - if completion.Usage.CompletionTokens > 0 { - completion.Usage = cumulativeUsage - } - - w.Header().Set("Content-Type", "application/json") - out, err := json.Marshal(completion) - if err != nil { - out, _ = json.Marshal(s.newErrorResponse(xerrors.Errorf("failed to marshal response: %w", err))) - w.WriteHeader(http.StatusInternalServerError) - } else { - w.WriteHeader(http.StatusOK) - } - - _, _ = w.Write(out) - - return nil -} diff --git a/aibridged/session_openai_chat_streaming.go b/aibridged/session_openai_chat_streaming.go deleted file mode 100644 index c47d6bf9d141d..0000000000000 --- a/aibridged/session_openai_chat_streaming.go +++ /dev/null @@ -1,241 +0,0 @@ -package aibridged - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "strings" - "sync" - - "github.com/openai/openai-go" - "github.com/openai/openai-go/packages/ssestream" - "golang.org/x/xerrors" - - "cdr.dev/slog" -) - -var _ Session = &OpenAIStreamingChatSession{} - -type OpenAIStreamingChatSession struct { - OpenAIChatSessionBase -} - -func NewOpenAIStreamingChatSession(req *ChatCompletionNewParamsWrapper, baseURL, key string) *OpenAIStreamingChatSession { - return &OpenAIStreamingChatSession{OpenAIChatSessionBase: OpenAIChatSessionBase{ - req: req, - baseURL: baseURL, - key: key, - }} -} - -func (s *OpenAIStreamingChatSession) Init(logger slog.Logger, tracker Tracker, toolMgr ToolManager) string { - return s.OpenAIChatSessionBase.Init(logger.Named("streaming"), tracker, toolMgr) -} - -func (s *OpenAIStreamingChatSession) ProcessRequest(w http.ResponseWriter, r *http.Request) error { - if s.req == nil { - return xerrors.Errorf("developer error: req is nil") - } - - // Include token usage. - s.req.StreamOptions.IncludeUsage = openai.Bool(true) - - s.injectTools() - - // Allow us to interrupt watch via cancel. - ctx, cancel := context.WithCancel(r.Context()) - defer cancel() - r = r.WithContext(ctx) // Rewire context for SSE cancellation. - - client := newOpenAIClient(s.baseURL, s.key) - logger := s.logger.With(slog.F("model", s.req.Model)) - - streamCtx, streamCancel := context.WithCancelCause(ctx) - defer streamCancel(xerrors.New("deferred")) - - events := newEventStream(openAIEventStream) - - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - defer func() { - if err := events.Close(streamCtx); err != nil { - logger.Error(ctx, "error closing stream", slog.Error(err)) - } - }() - - BasicSSESender(streamCtx, events, logger.Named("sse-sender")).ServeHTTP(w, r) - }() - - // TODO: implement parallel tool calls. - // TODO: don't send if not supported by model (i.e. o4-mini). - if len(s.req.Tools) > 0 { // If no tools are specified but this setting is set, it'll cause a 400 Bad Request. - s.req.ParallelToolCalls = openai.Bool(false) - } - - prompt, err := s.req.LastUserPrompt() - if err != nil { - logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err)) - } - - var ( - stream *ssestream.Stream[openai.ChatCompletionChunk] - cumulativeUsage openai.CompletionUsage - ) - for { - var pendingToolCalls []openai.FinishedChatCompletionToolCall - - stream = client.Chat.Completions.NewStreaming(ctx, s.req.ChatCompletionNewParams) - var acc openai.ChatCompletionAccumulator - for stream.Next() { - chunk := stream.Current() - acc.AddChunk(chunk) - - shouldRelayChunk := true - if toolCall, ok := acc.JustFinishedToolCall(); ok { - // Don't intercept and handle builtin tools. - if s.toolMgr.GetTool(toolCall.Name) != nil { - pendingToolCalls = append(pendingToolCalls, toolCall) - // Don't relay this chunk back; we'll handle it transparently. - shouldRelayChunk = false - } else { - if err := s.tracker.TrackToolUsage(ctx, s.id, chunk.ID, toolCall.Name, toolCall.Arguments, false, nil); err != nil { - logger.Warn(ctx, "failed to track tool usage", slog.Error(err)) - } - } - } - - if len(pendingToolCalls) > 0 { - // Any chunks following a tool call invocation should not be relayed. - shouldRelayChunk = false - } - - cumulativeUsage = sumUsage(cumulativeUsage, chunk.Usage) - - if shouldRelayChunk { - // If usage information is available, relay the cumulative usage once all tool invocations have completed. - if chunk.Usage.CompletionTokens > 0 { - chunk.Usage = cumulativeUsage - } - - // Overwrite response identifier since proxy obscures injected tool call invocations. - chunk.ID = s.id - events.TrySend(ctx, chunk) - } - } - - if prompt != nil { - if err := s.tracker.TrackPromptUsage(ctx, s.id, acc.ID, *prompt, nil); err != nil { - logger.Warn(ctx, "failed to track prompt usage", slog.Error(err)) - } - } - - // If the usage information is set, track it. - // The API will send usage information when the response terminates, which will happen if a tool call is invoked. - if err := s.tracker.TrackTokensUsage(ctx, s.id, acc.ID, cumulativeUsage.PromptTokens, cumulativeUsage.CompletionTokens, Metadata{ - "prompt_audio": cumulativeUsage.PromptTokensDetails.AudioTokens, - "prompt_cached": cumulativeUsage.PromptTokensDetails.CachedTokens, - "completion_accepted_prediction": cumulativeUsage.CompletionTokensDetails.AcceptedPredictionTokens, - "completion_rejected_prediction": cumulativeUsage.CompletionTokensDetails.RejectedPredictionTokens, - "completion_audio": cumulativeUsage.CompletionTokensDetails.AudioTokens, - "completion_reasoning": cumulativeUsage.CompletionTokensDetails.ReasoningTokens, - }); err != nil { - logger.Warn(ctx, "failed to track tokens usage", slog.Error(err)) - } - - if err := stream.Err(); err != nil { - logger.Error(ctx, "server stream error", slog.Error(err)) - var apierr *openai.Error - if errors.As(err, &apierr) { - events.TrySend(ctx, s.newErrorResponse(err)) - break - } else if isConnectionError(err) { - logger.Warn(ctx, "upstream connection error", slog.Error(err)) - } - - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - if len(pendingToolCalls) == 0 { - break - } - - appendedPrevMsg := false - for _, tc := range pendingToolCalls { - tool := s.toolMgr.GetTool(tc.Name) - if tool == nil { - // Not a known tool, don't do anything. - logger.Warn(ctx, "pending tool call for non-managed tool, skipping", slog.F("tool", tc.Name)) - continue - } - - // Only do this once. - if !appendedPrevMsg { - // Append the whole message from this stream as context since we'll be sending a new request with the tool results. - s.req.Messages = append(s.req.Messages, acc.Choices[len(acc.Choices)-1].Message.ToParam()) - appendedPrevMsg = true - } - - var toolName string - _, toolName, err = DecodeToolID(tc.Name) - if err != nil { - logger.Debug(ctx, "failed to decode tool ID", slog.Error(err), slog.F("name", tc.Name)) - toolName = tc.Name - } - - if err := s.tracker.TrackToolUsage(ctx, s.id, acc.ID, toolName, tc.Arguments, true, nil); err != nil { - logger.Warn(ctx, "failed to track tool usage", slog.Error(err), slog.F("tool", tool.Name)) - } - - var args map[string]any - if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil { - logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err), slog.F("tool", toolName)) - } - - res, err := tool.Call(streamCtx, args) - if err != nil { - // Always provide a tool_result even if the tool call failed. - errorJSON, _ := json.Marshal(s.newErrorResponse(err)) - s.req.Messages = append(s.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - var out strings.Builder - if err := json.NewEncoder(&out).Encode(res); err != nil { - logger.Error(ctx, "failed to encode tool response", slog.Error(err)) - // Always provide a tool_result even if encoding failed. - errorJSON, _ := json.Marshal(s.newErrorResponse(err)) - s.req.Messages = append(s.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID)) - continue - } - - s.req.Messages = append(s.req.Messages, openai.ToolMessage(out.String(), tc.ID)) - } - } - - err = events.Close(streamCtx) - if err != nil { - logger.Error(ctx, "failed to close event stream", slog.Error(err)) - } - - wg.Wait() - - // Ensure we flush all the remaining data before ending. - flush(w) - - if err != nil { - streamCancel(xerrors.Errorf("stream err: %w", err)) - } else { - streamCancel(xerrors.New("gracefully done")) - } - - <-streamCtx.Done() - return nil -} - -func (s *OpenAIStreamingChatSession) Close() error { - return nil // TODO: do we even need this? -} diff --git a/aibridged/sse_parser.go b/aibridged/sse_parser.go deleted file mode 100644 index 63417cd1d9e60..0000000000000 --- a/aibridged/sse_parser.go +++ /dev/null @@ -1,124 +0,0 @@ -package aibridged - -import ( - "bufio" - "io" - "strconv" - "strings" - "sync" -) - -const ( - SSEEventTypeMessage = "message" - SSEEventTypeError = "error" - SSEEventTypePing = "ping" -) - -type SSEEvent struct { - Type string - Data string - ID string - Retry int -} - -type SSEParser struct { - events map[string][]SSEEvent - mu sync.RWMutex -} - -func NewSSEParser() *SSEParser { - return &SSEParser{ - events: make(map[string][]SSEEvent), - } -} - -func (p *SSEParser) Parse(reader io.Reader) error { - scanner := bufio.NewScanner(reader) - - var currentEvent SSEEvent - var dataLines []string - - for scanner.Scan() { - line := scanner.Text() - - // Empty line indicates end of event - if line == "" { - if len(dataLines) > 0 { - currentEvent.Data = strings.Join(dataLines, "\n") - } - - // Default to message type if no event type specified - if currentEvent.Type == "" { - currentEvent.Type = SSEEventTypeMessage - } - - // Store the event - p.mu.Lock() - p.events[currentEvent.Type] = append(p.events[currentEvent.Type], currentEvent) - p.mu.Unlock() - - // Reset for next event - currentEvent = SSEEvent{} - dataLines = nil - continue - } - - // Skip comments - if strings.HasPrefix(line, ":") { - continue - } - - // Parse field:value format - if colonIndex := strings.Index(line, ":"); colonIndex != -1 { - field := line[:colonIndex] - value := line[colonIndex+1:] - - // Remove leading space from value if present - if len(value) > 0 && value[0] == ' ' { - value = value[1:] - } - - switch field { - case "event": - currentEvent.Type = value - case "data": - dataLines = append(dataLines, value) - case "id": - currentEvent.ID = value - case "retry": - if retryMs, err := strconv.Atoi(value); err == nil { - currentEvent.Retry = retryMs - } - } - } - } - - return scanner.Err() -} - -func (p *SSEParser) EventsByType(eventType string) []SSEEvent { - p.mu.RLock() - defer p.mu.RUnlock() - - events := p.events[eventType] - result := make([]SSEEvent, len(events)) - copy(result, events) - return result -} - -func (p *SSEParser) MessageEvents() []SSEEvent { - return p.EventsByType(SSEEventTypeMessage) -} - -func (p *SSEParser) AllEvents() map[string][]SSEEvent { - p.mu.RLock() - defer p.mu.RUnlock() - - result := make(map[string][]SSEEvent) - for eventType, events := range p.events { - eventsCopy := make([]SSEEvent, len(events)) - copy(eventsCopy, events) - result[eventType] = eventsCopy - } - return result -} diff --git a/aibridged/streaming.go b/aibridged/streaming.go deleted file mode 100644 index c1e813f574b0a..0000000000000 --- a/aibridged/streaming.go +++ /dev/null @@ -1,217 +0,0 @@ -package aibridged - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "sync" - "syscall" - - "golang.org/x/xerrors" - - "cdr.dev/slog" -) - -// isConnectionError checks if an error is related to client disconnection -func isConnectionError(err error) bool { - if err == nil { - return false - } - - if errors.Is(err, io.EOF) { - return true - } - - if errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.EPIPE) { - return true - } - - errStr := err.Error() - return strings.Contains(errStr, "broken pipe") || - strings.Contains(errStr, "connection reset by peer") || - strings.Contains(errStr, "use of closed network connection") -} - -// BasicSSESender was implemented to overcome httpapi.ServerSentEventSender's odd design choices. For example, it doesn't -// write "event: data" for every data event (it's unnecessary, and breaks some AI tools' parsing of the SSE stream). -func BasicSSESender(outerCtx context.Context, stream EventStreamer, logger slog.Logger) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("X-Accel-Buffering", "no") - - // Send initial flush to ensure connection is established. - flush(w) - - for { - select { - case <-outerCtx.Done(): - return - case <-ctx.Done(): - fmt.Printf("request done, reason: %q\n", ctx.Err()) - return - case <-stream.Closed(): - return - case ev, ok := <-stream.Events(): - if !ok { - return - } - - _, err := w.Write(ev) - if err != nil { - if isConnectionError(err) { - logger.Debug(ctx, "client disconnected during SSE write", slog.Error(err)) - } else { - logger.Error(ctx, "failed to write SSE event", slog.Error(err)) - } - return - } - flush(w) - } - } - } -} - -func flush(w http.ResponseWriter) { - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "SSE not supported", http.StatusInternalServerError) - return - } - - if flusher == nil { - return - } - - defer func() { - if r := recover(); r != nil { - // Silently handle panic from flush, likely due to broken connection - } - }() - - flusher.Flush() -} - -type EventStreamer interface { - TrySend(ctx context.Context, data any) error - Events() <-chan event - Close(ctx context.Context) error - Closed() <-chan any -} - -type event []byte - -type eventStream struct { - eventsCh chan event - kind eventStreamProvider - - closedOnce sync.Once - closedCh chan any -} - -type eventStreamProvider string - -const ( - openAIEventStream eventStreamProvider = ProviderOpenAI - anthropicEventStream eventStreamProvider = ProviderAnthropic -) - -func newEventStream(kind eventStreamProvider) *eventStream { - return &eventStream{ - kind: kind, - eventsCh: make(chan event), - closedCh: make(chan any), - } -} - -func (s *eventStream) Events() <-chan event { - return s.eventsCh -} - -func (s *eventStream) Closed() <-chan any { - return s.closedCh -} - -func (s *eventStream) TrySend(ctx context.Context, data any) error { - // Save an unnecessary marshaling if possible. - select { - case <-ctx.Done(): - return ctx.Err() - case <-s.closedCh: - return xerrors.New("closed") - default: - } - - payload, err := json.Marshal(data) - if err != nil { - return xerrors.Errorf("marshal payload: %w", err) - } - - return s.send(ctx, payload) -} - -func (s *eventStream) send(ctx context.Context, payload []byte) error { - switch s.kind { - case openAIEventStream: - var buf bytes.Buffer - buf.WriteString("data: ") - buf.Write(payload) - buf.WriteString("\n\n") - payload = buf.Bytes() - case anthropicEventStream: - // TODO: improve this approach. - type msgType struct { - Val string `json:"type"` - } - var typ msgType - if err := json.NewDecoder(bytes.NewBuffer(payload)).Decode(&typ); err != nil { - return xerrors.Errorf("failed to determine anthropic event type for %q: %w", payload, err) - } - - var buf bytes.Buffer - buf.WriteString("event: ") - buf.WriteString(typ.Val) - buf.WriteString("\n") - buf.WriteString("data: ") - buf.Write(payload) - buf.WriteString("\n\n") - payload = buf.Bytes() - default: - return xerrors.Errorf("unknown stream kind: %q", s.kind) - } - - select { - case <-ctx.Done(): - return ctx.Err() - case <-s.closedCh: - return xerrors.New("closed") - case s.eventsCh <- payload: - return nil - } -} - -func (s *eventStream) Close(ctx context.Context) error { - var out error - s.closedOnce.Do(func() { - switch s.kind { - case openAIEventStream: - err := s.send(ctx, []byte("[DONE]")) - if err != nil { - out = xerrors.Errorf("close stream: %w", err) - } - } - - close(s.closedCh) - close(s.eventsCh) - }) - - return out -} diff --git a/aibridged/tool_manager.go b/aibridged/tool_manager.go deleted file mode 100644 index 415e8be3611b8..0000000000000 --- a/aibridged/tool_manager.go +++ /dev/null @@ -1,78 +0,0 @@ -package aibridged - -import ( - "sync" -) - -type ToolManager interface { - AddTools(server string, tools []*MCPTool) - GetTool(name string) *MCPTool - ListTools() []*MCPTool -} - -type ToolRegistry map[string][]*MCPTool - -var _ ToolManager = &InjectedToolManager{} - -// InjectedToolManager is responsible for all injected tools. -type InjectedToolManager struct { - mu sync.RWMutex - tools map[string]*MCPTool -} - -// -// -// -// -// TODO: need to inject tools along with their server name -// -// -// -// - -func NewInjectedToolManager(tools ToolRegistry) *InjectedToolManager { - tm := &InjectedToolManager{} - for server, val := range tools { - tm.AddTools(server, val) - } - return tm -} - -func (t *InjectedToolManager) AddTools(server string, tools []*MCPTool) { - t.mu.Lock() - defer t.mu.Unlock() - - if t.tools == nil { - t.tools = make(map[string]*MCPTool, len(tools)) - } - - for _, tool := range tools { - t.tools[EncodeToolID(server, tool.Name)] = tool - } -} - -func (t *InjectedToolManager) GetTool(name string) *MCPTool { - t.mu.RLock() - defer t.mu.RUnlock() - - if t.tools == nil { - return nil - } - - return t.tools[name] -} - -func (t *InjectedToolManager) ListTools() []*MCPTool { - t.mu.RLock() - defer t.mu.RUnlock() - - if t.tools == nil { - return nil - } - - var out []*MCPTool - for _, tool := range t.tools { - out = append(out, tool) - } - return out -} diff --git a/aibridged/tracker.go b/aibridged/tracker.go deleted file mode 100644 index 333d0ae3aa829..0000000000000 --- a/aibridged/tracker.go +++ /dev/null @@ -1,96 +0,0 @@ -package aibridged - -import ( - "context" - "encoding/json" - - "golang.org/x/xerrors" - "google.golang.org/protobuf/types/known/anypb" - "google.golang.org/protobuf/types/known/structpb" - - "github.com/coder/coder/v2/aibridged/proto" -) - -type Metadata map[string]any - -func (m Metadata) MarshalForProto() map[string]*anypb.Any { - if len(m) == 0 { - return nil - } - out := make(map[string]*anypb.Any, len(m)) - for k, v := range m { - if sv, err := structpb.NewValue(v); err == nil { - if av, err := anypb.New(sv); err == nil { - out[k] = av - } - } - } - return out -} - -type Tracker interface { - TrackTokensUsage(ctx context.Context, sessionID, msgID string, promptTokens, completionTokens int64, metadata Metadata) error - TrackPromptUsage(ctx context.Context, sessionID, msgID, prompt string, metadata Metadata) error - TrackToolUsage(ctx context.Context, sessionID, msgID, name string, args any, injected bool, metadata Metadata) error -} - -var _ Tracker = &DRPCTracker{} - -// DRPCTracker tracks usage by calling RPC endpoints on a given dRPC client. -type DRPCTracker struct { - client proto.DRPCAIBridgeDaemonClient -} - -func NewDRPCTracker(client proto.DRPCAIBridgeDaemonClient) *DRPCTracker { - return &DRPCTracker{client} -} - -func (d *DRPCTracker) TrackTokensUsage(ctx context.Context, sessionID, msgID string, promptTokens, completionTokens int64, metadata Metadata) error { - _, err := d.client.TrackTokenUsage(ctx, &proto.TrackTokenUsageRequest{ - SessionId: sessionID, - MsgId: msgID, - InputTokens: promptTokens, - OutputTokens: completionTokens, - Metadata: metadata.MarshalForProto(), - }) - return err -} - -func (d *DRPCTracker) TrackPromptUsage(ctx context.Context, sessionID, msgID string, prompt string, metadata Metadata) error { - _, err := d.client.TrackUserPrompt(ctx, &proto.TrackUserPromptRequest{ - SessionId: sessionID, - MsgId: msgID, - Prompt: prompt, - Metadata: metadata.MarshalForProto(), - }) - return err -} - -func (d *DRPCTracker) TrackToolUsage(ctx context.Context, sessionID, msgID, name string, args any, injected bool, metadata Metadata) error { - var ( - serialized []byte - err error - ) - - switch val := args.(type) { - case string: - serialized = []byte(val) - case []byte: - serialized = val - default: - serialized, err = json.Marshal(args) - if err != nil { - return xerrors.Errorf("marshal tool usage args: %w", err) - } - } - - _, err = d.client.TrackToolUsage(ctx, &proto.TrackToolUsageRequest{ - SessionId: sessionID, - MsgId: msgID, - Tool: name, - Input: string(serialized), - Injected: injected, - Metadata: metadata.MarshalForProto(), - }) - return err -} diff --git a/cli/server.go b/cli/server.go index 2e4be2fa7d5c7..748c81b6aba1a 100644 --- a/cli/server.go +++ b/cli/server.go @@ -63,8 +63,9 @@ import ( "github.com/coder/serpent" "github.com/coder/wgtunnel/tunnelsdk" + "github.com/coder/aibridge" + aibridgeproto "github.com/coder/aibridge/proto" "github.com/coder/coder/v2/aibridged" - aibridgedproto "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/notifications/reports" "github.com/coder/coder/v2/coderd/runtimeconfig" @@ -1127,7 +1128,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. aiBridgeDaemons = append(aiBridgeDaemons, daemon) } coderAPI.AIBridgeDaemons = aiBridgeDaemons - coderAPI.AIBridges = tlru.New[string](tlru.ConstantCost[*aibridged.Bridge], 100) // TODO: configurable. + coderAPI.AIBridges = tlru.New[string](tlru.ConstantCost[*aibridge.Bridge], 100) // TODO: configurable. // Updates the systemd status from activating to activated. _, err = daemon.SdNotify(false, daemon.SdNotifyReady) @@ -1573,12 +1574,11 @@ func newProvisionerDaemon( } func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string, bridgeCfg codersdk.AIBridgeConfig) (*aibridged.Server, error) { - httpAddr := "0.0.0.0:0" // TODO: configurable. - return aibridged.New(func(dialCtx context.Context) (aibridgedproto.DRPCAIBridgeDaemonClient, error) { + return aibridged.New(func(dialCtx context.Context) (aibridgeproto.DRPCStoreClient, error) { // This debounces calls to listen every second. // TODO: is this true / necessary? return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx, name) - }, httpAddr, coderAPI.Logger.Named("aibridged").With(slog.F("name", name))) + }, coderAPI.Logger.Named("aibridged").With(slog.F("name", name))) } // nolint: revive diff --git a/coderd/aibridge.go b/coderd/aibridge.go index aa0100af3d33a..55d625b6e67ad 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -11,8 +11,9 @@ import ( "cdr.dev/slog" + "github.com/coder/aibridge" + "github.com/coder/aibridge/proto" "github.com/coder/coder/v2/aibridged" - aibridgedproto "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/coderd/util/slice" ) @@ -50,7 +51,7 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { return } - key, ok := r.Context().Value(aibridged.ContextKeyBridgeAPIKey{}).(string) + key, ok := r.Context().Value(aibridge.ContextKeyBridgeAPIKey{}).(string) if key == "" || !ok { http.Error(rw, "unable to retrieve request session key", http.StatusBadRequest) return @@ -65,7 +66,7 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { http.StripPrefix("/api/v2/aibridge", bridge.Handler()).ServeHTTP(rw, r) } -func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, clientFn func() (aibridgedproto.DRPCAIBridgeDaemonClient, error)) (*aibridged.Bridge, error) { +func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, clientFn func() (proto.DRPCStoreClient, error)) (*aibridge.Bridge, error) { if api.AIBridges == nil { return nil, xerrors.New("bridge cache storage is not configured") } @@ -86,11 +87,11 @@ func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, cli } // TODO: only instantiate once. - registry := aibridged.ProviderRegistry{ - aibridged.ProviderOpenAI: aibridged.NewOpenAIProvider(api.DeploymentValues.AI.BridgeConfig.OpenAI.BaseURL.String(), api.DeploymentValues.AI.BridgeConfig.OpenAI.Key.String()), - aibridged.ProviderAnthropic: aibridged.NewAnthropicMessagesProvider(api.DeploymentValues.AI.BridgeConfig.Anthropic.BaseURL.String(), api.DeploymentValues.AI.BridgeConfig.Anthropic.Key.String()), + registry := aibridge.ProviderRegistry{ + aibridge.ProviderOpenAI: aibridge.NewOpenAIProvider(api.DeploymentValues.AI.BridgeConfig.OpenAI.BaseURL.String(), api.DeploymentValues.AI.BridgeConfig.OpenAI.Key.String()), + aibridge.ProviderAnthropic: aibridge.NewAnthropicMessagesProvider(api.DeploymentValues.AI.BridgeConfig.Anthropic.BaseURL.String(), api.DeploymentValues.AI.BridgeConfig.Anthropic.Key.String()), } - bridge, err := aibridged.NewBridge(registry, api.Logger.Named("ai_bridge"), clientFn, tools) + bridge, err := aibridge.NewBridge(registry, api.Logger.Named("ai_bridge"), clientFn, tools) if err != nil { return nil, xerrors.Errorf("create new bridge server: %w", err) } @@ -104,9 +105,9 @@ func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, cli return val, nil } -func (api *API) fetchTools(ctx context.Context, logger slog.Logger, key string) (map[string][]*aibridged.MCPTool, error) { +func (api *API) fetchTools(ctx context.Context, logger slog.Logger, key string) (map[string][]*aibridge.MCPTool, error) { url := api.DeploymentValues.AccessURL.String() + "/api/experimental/mcp/http" - coderMCP, err := aibridged.NewMCPToolBridge("coder", url, map[string]string{ + coderMCP, err := aibridge.NewMCPToolBridge("coder", url, map[string]string{ "Coder-Session-Token": key, }, logger.Named("mcp-bridge-coder")) if err != nil { @@ -131,7 +132,7 @@ func (api *API) fetchTools(ctx context.Context, logger slog.Logger, key string) return nil, xerrors.Errorf("MCP proxy init: %w", err) } - return map[string][]*aibridged.MCPTool{ + return map[string][]*aibridge.MCPTool{ "coder": coderMCP.ListTools(), }, nil } diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index 5e566fa70f2e0..03de6d182584a 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -11,11 +11,11 @@ import ( "cdr.dev/slog" - "github.com/coder/coder/v2/aibridged/proto" + proto "github.com/coder/aibridge/proto" "github.com/coder/coder/v2/coderd/database" ) -var _ proto.DRPCAIBridgeDaemonServer = &Server{} +var _ proto.DRPCStoreServer = &Server{} type Server struct { // lifecycleCtx must be tied to the API server's lifecycle @@ -34,8 +34,8 @@ func NewServer(lifecycleCtx context.Context, store database.Store, logger slog.L }, nil } -// StartSession implements proto.DRPCAIBridgeDaemonServer. -func (s *Server) StartSession(ctx context.Context, in *proto.StartSessionRequest) (*proto.StartSessionResponse, error) { +// StoreSession implements proto.DRPCStoreServer. +func (s *Server) StoreSession(ctx context.Context, in *proto.StoreSessionRequest) (*proto.StoreSessionResponse, error) { sessID, err := uuid.Parse(in.GetSessionId()) if err != nil { return nil, xerrors.Errorf("invalid session ID %q: %w", in.GetSessionId(), err) @@ -55,7 +55,7 @@ func (s *Server) StartSession(ctx context.Context, in *proto.StartSessionRequest return nil, xerrors.Errorf("start session: %w", err) } - return &proto.StartSessionResponse{}, nil + return &proto.StoreSessionResponse{}, nil } func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { diff --git a/coderd/coderd.go b/coderd/coderd.go index c05a574c91ca7..0b24a46e45385 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -22,6 +22,8 @@ import ( "github.com/ammario/tlru" + aibridgeproto "github.com/coder/aibridge/proto" + "github.com/coder/coder/v2/aibridged" "github.com/coder/coder/v2/coderd/oauth2provider" "github.com/coder/coder/v2/coderd/pproflabel" "github.com/coder/coder/v2/coderd/prebuilds" @@ -45,7 +47,7 @@ import ( "tailscale.com/types/key" "tailscale.com/util/singleflight" - "github.com/coder/coder/v2/aibridged" + "github.com/coder/aibridge" "github.com/coder/coder/v2/coderd/aibridgedserver" provisionerdproto "github.com/coder/coder/v2/provisionerd/proto" @@ -63,7 +65,6 @@ import ( "github.com/coder/coder/v2/coderd/webpush" agentproto "github.com/coder/coder/v2/agent/proto" - aibridgedproto "github.com/coder/coder/v2/aibridged/proto" "github.com/coder/coder/v2/buildinfo" _ "github.com/coder/coder/v2/coderd/apidoc" // Used for swagger docs. "github.com/coder/coder/v2/coderd/appearance" @@ -1738,7 +1739,7 @@ type API struct { dbRolluper *dbrollup.Rolluper AIBridgeDaemons []*aibridged.Server - AIBridges *tlru.Cache[string, *aibridged.Bridge] + AIBridges *tlru.Cache[string, *aibridge.Bridge] AIBridgesMu sync.RWMutex } @@ -1969,7 +1970,7 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n return provisionerdproto.NewDRPCProvisionerDaemonClient(clientSession), nil } -func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name string) (client aibridgedproto.DRPCAIBridgeDaemonClient, err error) { +func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name string) (client aibridgeproto.DRPCStoreClient, err error) { // TODO(dannyk): implement options. // TODO(dannyk): implement tracing. @@ -1991,7 +1992,7 @@ func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name strin if err != nil { return nil, err } - err = aibridgedproto.DRPCRegisterAIBridgeDaemon(mux, srv) + err = aibridgeproto.DRPCRegisterStore(mux, srv) if err != nil { return nil, err } @@ -2024,7 +2025,7 @@ func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context, name strin _ = serverSession.Close() }() - return aibridgedproto.NewDRPCAIBridgeDaemonClient(clientSession), nil + return aibridgeproto.NewDRPCStoreClient(clientSession), nil } func (api *API) DERPMap() *tailcfg.DERPMap { diff --git a/go.mod b/go.mod index f2cdff150268b..f7ce0e9c6ca0e 100644 --- a/go.mod +++ b/go.mod @@ -196,7 +196,7 @@ require ( go.uber.org/mock v0.5.0 go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 golang.org/x/crypto v0.41.0 - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 + golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 golang.org/x/mod v0.27.0 golang.org/x/net v0.43.0 golang.org/x/oauth2 v0.30.0 @@ -208,14 +208,14 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da google.golang.org/api v0.246.0 google.golang.org/grpc v1.74.2 - google.golang.org/protobuf v1.36.6 + google.golang.org/protobuf v1.36.7 gopkg.in/DataDog/dd-trace-go.v1 v1.74.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc kernel.org/pub/linux/libs/security/libcap/cap v1.2.73 - storj.io/drpc v0.0.33 - tailscale.com v1.80.3 + storj.io/drpc v0.0.34 + tailscale.com v1.86.4 ) require ( @@ -466,7 +466,10 @@ require ( require github.com/coder/clistat v1.0.0 -require github.com/SherClockHolmes/webpush-go v1.4.0 +require ( + github.com/SherClockHolmes/webpush-go v1.4.0 + github.com/coder/aibridge v0.0.0 +) require ( github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect @@ -477,7 +480,6 @@ require ( ) require ( - github.com/anthropics/anthropic-sdk-go v1.4.0 github.com/brianvoe/gofakeit/v7 v7.3.0 github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 github.com/coder/aisdk-go v0.0.9 @@ -485,10 +487,12 @@ require ( github.com/fsnotify/fsnotify v1.9.0 github.com/go-git/go-git/v5 v5.16.2 github.com/mark3labs/mcp-go v0.37.0 - github.com/openai/openai-go v1.8.1 - github.com/tidwall/sjson v1.2.5 + github.com/tidwall/sjson v1.2.5 // indirect ) +// aibridge-related deps and directives. +replace github.com/coder/aibridge v0.0.0 => /home/coder/aibridge + require ( cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.120.0 // indirect @@ -503,6 +507,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/anthropics/anthropic-sdk-go v1.4.0 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aquasecurity/trivy v0.58.2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect @@ -526,6 +531,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/openai/openai-go v1.12.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/samber/lo v1.50.0 // indirect diff --git a/go.sum b/go.sum index ed8bb97cca53d..c91ed089ebf8a 100644 --- a/go.sum +++ b/go.sum @@ -1625,8 +1625,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1/go.mod h1:01TvyaK8x640crO2iFwW/6CFCZgNsOvOGH3B5J239m0= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1 h1:TCyOus9tym82PD1VYtthLKMVMlVyRwtDI4ck4SR2+Ok= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1/go.mod h1:Z/S1brD5gU2Ntht/bHxBVnGxXKTvZDr0dNv/riUzPmY= -github.com/openai/openai-go v1.8.1 h1:mGS5Y9dEeHvLnE3k9LF4vUV3pvYG2K/6MHI/fCr4Ou8= -github.com/openai/openai-go v1.8.1/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= +github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -2031,8 +2031,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4= +golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -2707,8 +2707,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/DataDog/dd-trace-go.v1 v1.74.0 h1:wScziU1ff6Bnyr8MEyxATPSLJdnLxKz3p6RsA8FUaek= gopkg.in/DataDog/dd-trace-go.v1 v1.74.0/go.mod h1:ReNBsNfnsjVC7GsCe80zRcykL/n+nxvsNrg3NbjuleM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -2800,5 +2800,5 @@ sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= -storj.io/drpc v0.0.33 h1:yCGZ26r66ZdMP0IcTYsj7WDAUIIjzXk6DJhbhvt9FHI= -storj.io/drpc v0.0.33/go.mod h1:vR804UNzhBa49NOJ6HeLjd2H3MakC1j5Gv8bsOQT6N4= +storj.io/drpc v0.0.34 h1:q9zlQKfJ5A7x8NQNFk8x7eKUF78FMhmAbZLnFK+og7I= +storj.io/drpc v0.0.34/go.mod h1:Y9LZaa8esL1PW2IDMqJE7CFSNq7d5bQ3RI7mGPtmKMg= From d73c25e96557706d7f42966901a1bdd566d4eef8 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 13 Aug 2025 11:31:57 +0200 Subject: [PATCH 61/61] use API interface, pass initiator via header Signed-off-by: Danny Kopping --- aibridged/aibridged.go | 23 ++++++++++++----------- aibridged/context.go | 6 ++++++ aibridged/middleware.go | 5 ++--- cli/server.go | 3 +-- coderd/aibridge.go | 18 ++++++++++++++---- 5 files changed, 35 insertions(+), 20 deletions(-) create mode 100644 aibridged/context.go diff --git a/aibridged/aibridged.go b/aibridged/aibridged.go index 8f43db08c8715..fdde680c08fd3 100644 --- a/aibridged/aibridged.go +++ b/aibridged/aibridged.go @@ -15,15 +15,16 @@ import ( "cdr.dev/slog" "github.com/coder/retry" + "github.com/coder/aibridge" "github.com/coder/aibridge/proto" "github.com/coder/coder/v2/codersdk" ) -type Dialer func(ctx context.Context) (proto.DRPCStoreClient, error) +type Dialer func(ctx context.Context) (aibridge.APIClient, error) type Server struct { clientDialer Dialer - clientCh chan proto.DRPCStoreClient + clientCh chan aibridge.APIClient logger slog.Logger wg sync.WaitGroup @@ -50,7 +51,7 @@ type Server struct { shuttingDownCh chan struct{} } -var _ proto.DRPCStoreServer = &Server{} +var _ aibridge.APIServer = &Server{} func New(rpcDialer Dialer, logger slog.Logger) (*Server, error) { if rpcDialer == nil { @@ -61,7 +62,7 @@ func New(rpcDialer Dialer, logger slog.Logger) (*Server, error) { daemon := &Server{ logger: logger, clientDialer: rpcDialer, - clientCh: make(chan proto.DRPCStoreClient), + clientCh: make(chan aibridge.APIClient), closeContext: ctx, closeCancel: cancel, closedCh: make(chan struct{}), @@ -137,7 +138,7 @@ connectLoop: } } -func (s *Server) Client() (proto.DRPCStoreClient, error) { +func (s *Server) Client() (aibridge.APIClient, error) { select { case <-s.closeContext.Done(): return nil, xerrors.New("context closed") @@ -150,7 +151,7 @@ func (s *Server) Client() (proto.DRPCStoreClient, error) { } func (s *Server) StoreSession(ctx context.Context, in *proto.StoreSessionRequest) (*proto.StoreSessionResponse, error) { - out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCStoreClient) (*proto.StoreSessionResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client aibridge.APIClient) (*proto.StoreSessionResponse, error) { return client.StoreSession(ctx, in) }) if err != nil { @@ -160,7 +161,7 @@ func (s *Server) StoreSession(ctx context.Context, in *proto.StoreSessionRequest } func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageRequest) (*proto.TrackTokenUsageResponse, error) { - out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCStoreClient) (*proto.TrackTokenUsageResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client aibridge.APIClient) (*proto.TrackTokenUsageResponse, error) { return client.TrackTokenUsage(ctx, in) }) if err != nil { @@ -170,7 +171,7 @@ func (s *Server) TrackTokenUsage(ctx context.Context, in *proto.TrackTokenUsageR } func (s *Server) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptRequest) (*proto.TrackUserPromptResponse, error) { - out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCStoreClient) (*proto.TrackUserPromptResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client aibridge.APIClient) (*proto.TrackUserPromptResponse, error) { return client.TrackUserPrompt(ctx, in) }) if err != nil { @@ -180,7 +181,7 @@ func (s *Server) TrackUserPrompt(ctx context.Context, in *proto.TrackUserPromptR } func (s *Server) TrackToolUsage(ctx context.Context, in *proto.TrackToolUsageRequest) (*proto.TrackToolUsageResponse, error) { - out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client proto.DRPCStoreClient) (*proto.TrackToolUsageResponse, error) { + out, err := clientDoWithRetries(ctx, s.Client, func(ctx context.Context, client aibridge.APIClient) (*proto.TrackToolUsageResponse, error) { return client.TrackToolUsage(ctx, in) }) if err != nil { @@ -202,8 +203,8 @@ func retryable(err error) bool { // expires. // NOTE: mostly copypasta from provisionerd; might be work abstracting. func clientDoWithRetries[T any](ctx context.Context, - getClient func() (proto.DRPCStoreClient, error), - f func(context.Context, proto.DRPCStoreClient) (T, error), + getClient func() (aibridge.APIClient, error), + f func(context.Context, aibridge.APIClient) (T, error), ) (ret T, _ error) { for retrier := retry.New(25*time.Millisecond, 5*time.Second); retrier.Wait(ctx); { var empty T diff --git a/aibridged/context.go b/aibridged/context.go new file mode 100644 index 0000000000000..7f049d331accc --- /dev/null +++ b/aibridged/context.go @@ -0,0 +1,6 @@ +package aibridged + +type ( + ContextKeyBridgeAPIKey struct{} + ContextKeyBridgeUserID struct{} +) diff --git a/aibridged/middleware.go b/aibridged/middleware.go index da8bf089ea10b..de2a511acbd55 100644 --- a/aibridged/middleware.go +++ b/aibridged/middleware.go @@ -6,7 +6,6 @@ import ( "crypto/subtle" "net/http" - "github.com/coder/aibridge" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpmw" ) @@ -37,8 +36,8 @@ func AuthMiddleware(db database.Store) func(http.Handler) http.Handler { } ctx = context.WithValue( - context.WithValue(ctx, aibridge.ContextKeyBridgeUserID{}, key.UserID), - aibridge.ContextKeyBridgeAPIKey{}, token) + context.WithValue(ctx, ContextKeyBridgeUserID{}, key.UserID), + ContextKeyBridgeAPIKey{}, token) // Pass request with modify context including the request token. next.ServeHTTP(rw, r.WithContext(ctx)) diff --git a/cli/server.go b/cli/server.go index 748c81b6aba1a..661db8cb28165 100644 --- a/cli/server.go +++ b/cli/server.go @@ -64,7 +64,6 @@ import ( "github.com/coder/wgtunnel/tunnelsdk" "github.com/coder/aibridge" - aibridgeproto "github.com/coder/aibridge/proto" "github.com/coder/coder/v2/aibridged" "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/notifications/reports" @@ -1574,7 +1573,7 @@ func newProvisionerDaemon( } func newAIBridgeDaemon(ctx context.Context, coderAPI *coderd.API, name string, bridgeCfg codersdk.AIBridgeConfig) (*aibridged.Server, error) { - return aibridged.New(func(dialCtx context.Context) (aibridgeproto.DRPCStoreClient, error) { + return aibridged.New(func(dialCtx context.Context) (aibridge.APIClient, error) { // This debounces calls to listen every second. // TODO: is this true / necessary? return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx, name) diff --git a/coderd/aibridge.go b/coderd/aibridge.go index 55d625b6e67ad..81b93211dfaea 100644 --- a/coderd/aibridge.go +++ b/coderd/aibridge.go @@ -11,8 +11,9 @@ import ( "cdr.dev/slog" + "github.com/google/uuid" + "github.com/coder/aibridge" - "github.com/coder/aibridge/proto" "github.com/coder/coder/v2/aibridged" "github.com/coder/coder/v2/coderd/util/slice" ) @@ -51,12 +52,21 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { return } - key, ok := r.Context().Value(aibridge.ContextKeyBridgeAPIKey{}).(string) + key, ok := r.Context().Value(aibridged.ContextKeyBridgeAPIKey{}).(string) if key == "" || !ok { http.Error(rw, "unable to retrieve request session key", http.StatusBadRequest) return } + userID, ok := r.Context().Value(aibridged.ContextKeyBridgeUserID{}).(uuid.UUID) + if !ok { + api.Logger.Error(r.Context(), "missing initiator ID in context") + http.Error(rw, "unable to retrieve initiator", http.StatusInternalServerError) + return + } + + r.Header.Set(aibridge.InitiatorHeaderKey, userID.String()) + bridge, err := api.createOrLoadBridgeForAPIKey(ctx, key, server.Client) if err != nil { api.Logger.Error(ctx, "failed to create ai bridge", slog.Error(err)) @@ -66,7 +76,7 @@ func (api *API) bridgeAIRequest(rw http.ResponseWriter, r *http.Request) { http.StripPrefix("/api/v2/aibridge", bridge.Handler()).ServeHTTP(rw, r) } -func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, clientFn func() (proto.DRPCStoreClient, error)) (*aibridge.Bridge, error) { +func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, apiClientFn func() (aibridge.APIClient, error)) (*aibridge.Bridge, error) { if api.AIBridges == nil { return nil, xerrors.New("bridge cache storage is not configured") } @@ -91,7 +101,7 @@ func (api *API) createOrLoadBridgeForAPIKey(ctx context.Context, key string, cli aibridge.ProviderOpenAI: aibridge.NewOpenAIProvider(api.DeploymentValues.AI.BridgeConfig.OpenAI.BaseURL.String(), api.DeploymentValues.AI.BridgeConfig.OpenAI.Key.String()), aibridge.ProviderAnthropic: aibridge.NewAnthropicMessagesProvider(api.DeploymentValues.AI.BridgeConfig.Anthropic.BaseURL.String(), api.DeploymentValues.AI.BridgeConfig.Anthropic.Key.String()), } - bridge, err := aibridge.NewBridge(registry, api.Logger.Named("ai_bridge"), clientFn, tools) + bridge, err := aibridge.NewBridge(registry, api.Logger.Named("ai_bridge"), apiClientFn, tools) if err != nil { return nil, xerrors.Errorf("create new bridge server: %w", err) }