diff --git a/pkg/gofr/datasource/openai/chatcompletion.go b/pkg/gofr/datasource/openai/chatcompletion.go new file mode 100644 index 000000000..074821e99 --- /dev/null +++ b/pkg/gofr/datasource/openai/chatcompletion.go @@ -0,0 +1,189 @@ +package openai + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +const CompletionsEndpoint = "/v1/chat/completions" + +type CreateCompletionsRequest struct { + Messages []Message `json:"messages,omitempty"` + Model string `json:"model,omitempty"` + Store bool `json:"store,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` + MetaData interface{} `json:"metadata,omitempty"` // object or null + FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` + LogitBias map[string]string `json:"logit_bias,omitempty"` + LogProbs int `json:"logprobs,omitempty"` + TopLogProbs int `json:"top_logprobs,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` // deprecated + MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` + N int `json:"n,omitempty"` + Modalities []string `json:"modalities,omitempty"` + Prediction interface{} `json:"prediction,omitempty"` + PresencePenalty float64 `json:"presence_penalty,omitempty"` + + Audio struct { + Voice string `json:"voice,omitempty"` + Format string `json:"format,omitempty"` + } `json:"audio,omitempty"` + + ResponseFormat interface{} `json:"response_format,omitempty"` + Seed int `json:"seed,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` + Stop interface{} `json:"stop,omitempty"` + Stream bool `json:"stream,omitempty"` + + StreamOptions struct { + IncludeUsage bool `json:"include_usage,omitempty"` + } `json:"stram_options,omitempty"` + + Temperature float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + + Tools []struct { + Type string `json:"type,omitempty"` + Function struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Parameters interface{} `json:"parameters,omitempty"` + Strict bool `json:"strict,omitempty"` + } `json:"function,omitempty"` + } `json:"tools,omitempty"` + + ToolChoice interface{} `json:"tool_choice,omitempty"` + ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"` + Suffix string `json:"suffix,omitempty"` + User string `json:"user,omitempty"` +} + +type Message struct { + Role string `json:"role,omitempty"` + Content string `json:"content,omitempty"` + Name string `json:"name,omitempty"` +} + +type CreateCompletionsResponse struct { + ID string `json:"id,omitempty"` + Object string `json:"object,omitempty"` + Created int `json:"created,omitempty"` + Model string `json:"model,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` + SystemFingerprint string `json:"system_fingerprint,omitempty"` + + Choices []struct { + Index int `json:"index,omitempty"` + + Message struct { + Role string `json:"role,omitempty"` + Content string `json:"content,omitempty"` + Refusal string `json:"refusal,omitempty"` + ToolCalls interface{} `json:"tool_calls,omitempty"` + } `json:"message"` + + Logprobs interface{} `json:"logprobs,omitempty"` + FinishReason string `json:"finish_reason,omitempty"` + } `json:"choices,omitempty"` + + Usage Usage `json:"usage,omitempty"` + + Error *Error `json:"error,omitempty"` +} + +type Usage struct { + PromptTokens int `json:"prompt_tokens,omitempty"` + CompletionTokens int `json:"completion_tokens,omitempty"` + TotalTokens int `json:"total_tokens,omitempty"` + CompletionTokensDetails interface{} `json:"completion_tokens_details,omitempty"` + PromptTokensDetails interface{} `json:"prompt_tokens_details,omitempty"` +} + +type Error struct { + Message string `json:"message,omitempty"` + Type string `json:"type,omitempty"` + Param interface{} `json:"param,omitempty"` + Code interface{} `json:"code,omitempty"` +} + +var ( + ErrMissingBoth = errors.New("both messages and model fields not provided") + ErrMissingMessages = errors.New("messages fields not provided") + ErrMissingModel = errors.New("model fields not provided") +) + +func (e *Error) Error() string { + return fmt.Sprintf("%s: %s", e.Code, e.Message) +} + +func (c *Client) CreateCompletionsRaw(ctx context.Context, r *CreateCompletionsRequest) ([]byte, error) { + return c.Post(ctx, CompletionsEndpoint, r) +} + +func (c *Client) CreateCompletions(ctx context.Context, r *CreateCompletionsRequest) (response *CreateCompletionsResponse, err error) { + tracerCtx, span := c.AddTrace(ctx, "CreateCompletions") + startTime := time.Now() + + if r.Messages == nil && r.Model == "" { + c.logger.Errorf("%v", ErrMissingBoth) + return nil, ErrMissingBoth + } + + if r.Messages == nil { + c.logger.Errorf("%v", ErrMissingMessages) + return nil, ErrMissingMessages + } + + if r.Model == "" { + c.logger.Errorf("%v", ErrMissingModel) + return nil, ErrMissingModel + } + + raw, err := c.CreateCompletionsRaw(tracerCtx, r) + if err != nil { + return response, err + } + + err = json.Unmarshal(raw, &response) + if err != nil { + return nil, err + } + + ql := &APILog{ + ID: response.ID, + Object: response.Object, + Created: response.Created, + Model: response.Model, + ServiceTier: response.ServiceTier, + SystemFingerprint: response.SystemFingerprint, + Usage: response.Usage, + Error: response.Error, + } + + c.SendChatCompletionOperationStats(ctx, ql, startTime, "ChatCompletion", span) + + return response, err +} + +func (c *Client) SendChatCompletionOperationStats(ctx context.Context, ql *APILog, startTime time.Time, method string, span trace.Span) { + duration := time.Since(startTime).Microseconds() + + ql.Duration = duration + + c.logger.Debug(ql) + + c.metrics.RecordHistogram(ctx, "openai_api_request_duration", float64(duration)) + c.metrics.RecordRequestCount(ctx, "openai_api_total_request_count") + c.metrics.RecordTokenUsage(ctx, "openai_api_token_usage", ql.Usage.PromptTokens, ql.Usage.CompletionTokens) + + if span != nil { + defer span.End() + span.SetAttributes(attribute.Int64(fmt.Sprintf("openai.%v.duration", method), duration)) + } +} diff --git a/pkg/gofr/datasource/openai/chatcompletion_test.go b/pkg/gofr/datasource/openai/chatcompletion_test.go new file mode 100644 index 000000000..20e1cdd90 --- /dev/null +++ b/pkg/gofr/datasource/openai/chatcompletion_test.go @@ -0,0 +1,141 @@ +package openai + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +//nolint:funlen // Function length is intentional due to complexity +func Test_ChatCompletions(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + tests := []struct { + name string + request *CreateCompletionsRequest + response *CreateCompletionsResponse + expectedError error + setupMocks func(*MockLogger, *MockMetrics) + }{ + { + name: "successful completion request", + request: &CreateCompletionsRequest{ + Messages: []Message{{Role: "user", Content: "Hello"}}, + Model: "gpt-3.5-turbo", + }, + response: &CreateCompletionsResponse{ + ID: "test-id", + Object: "chat.completion", + Created: 1234567890, + Usage: Usage{ + PromptTokens: 10, + CompletionTokens: 20, + TotalTokens: 30, + }, + }, + expectedError: nil, + setupMocks: func(logger *MockLogger, metrics *MockMetrics) { + metrics.EXPECT().RecordHistogram(gomock.Any(), "openai_api_request_duration", gomock.Any()) + metrics.EXPECT().RecordRequestCount(gomock.Any(), "openai_api_total_request_count") + metrics.EXPECT().RecordTokenUsage(gomock.Any(), "openai_api_token_usage", 10, 20) + logger.EXPECT().Debug(gomock.Any()) + }, + }, + { + name: "missing both messages and model", + request: &CreateCompletionsRequest{}, + expectedError: ErrMissingBoth, + setupMocks: func(logger *MockLogger, _ *MockMetrics) { + logger.EXPECT().Errorf("%v", ErrMissingBoth) + }, + }, + { + name: "missing messages", + request: &CreateCompletionsRequest{ + Model: "gpt-3.5-turbo", + }, + expectedError: ErrMissingMessages, + setupMocks: func(logger *MockLogger, _ *MockMetrics) { + logger.EXPECT().Errorf("%v", ErrMissingMessages) + }, + }, + { + name: "missing model", + request: &CreateCompletionsRequest{ + Messages: []Message{{Role: "user", Content: "Hello"}}, + }, + expectedError: ErrMissingModel, + setupMocks: func(logger *MockLogger, _ *MockMetrics) { + logger.EXPECT().Errorf("%v", ErrMissingModel) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + + var server *httptest.Server + + if tt.response != nil { + server = setupTestServer(t, CompletionsEndpoint, tt.response) + defer server.Close() + serverURL = server.URL + } + + client := &Client{ + config: &Config{ + APIKey: "test-api-key", + BaseURL: serverURL, + }, + httpClient: http.DefaultClient, + logger: mockLogger, + metrics: mockMetrics, + } + + tt.setupMocks(mockLogger, mockMetrics) + + response, err := client.CreateCompletions(context.Background(), tt.request) + + if tt.expectedError != nil { + require.ErrorIs(t, err, tt.expectedError) + assert.Nil(t, response) + } else { + require.NoError(t, err) + assert.NotNil(t, response) + } + }) + } +} + +func setupTestServer(t *testing.T, path string, response interface{}) *httptest.Server { + t.Helper() + + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, path, r.URL.Path) + assert.Equal(t, "Bearer test-api-key", r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(response) + + if err != nil { + t.Error(err) + return + } + })) + + return server +} diff --git a/pkg/gofr/datasource/openai/client.go b/pkg/gofr/datasource/openai/client.go new file mode 100644 index 000000000..d4db3c7c4 --- /dev/null +++ b/pkg/gofr/datasource/openai/client.go @@ -0,0 +1,186 @@ +package openai + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + + "go.opentelemetry.io/otel/trace" +) + +type Config struct { + APIKey string + Model string + BaseURL string + Timeout time.Duration + MaxIdleConns int +} + +type Client struct { + config *Config + logger Logger + metrics Metrics + tracer trace.Tracer + httpClient *http.Client +} + +var ( + ErrorMissingAPIKey = errors.New("api key not provided") +) + +type ClientOption func(*Client) + +func WithClientHTTP(httpClient *http.Client) func(*Client) { + return func(c *Client) { + c.httpClient = httpClient + } +} + +func WithClientTimeout(d time.Duration) func(*Client) { + return func(c *Client) { + c.httpClient.Timeout = d + } +} + +func NewClient(config *Config, opts ...ClientOption) (*Client, error) { + if config.APIKey == "" { + return nil, ErrorMissingAPIKey + } + + if config.BaseURL == "" { + config.BaseURL = "https://api.openai.com" + } + + // Use the provided HTTP client or create a new one with defaults + c := &Client{ + config: config, + httpClient: &http.Client{ + Timeout: config.Timeout, + Transport: &http.Transport{ + MaxIdleConns: config.MaxIdleConns, + IdleConnTimeout: 30 * time.Second, + }, + }, + } + + for _, opt := range opts { + opt(c) + } + + return c, nil +} + +func (c *Client) UseLogger(logger interface{}) { + if l, ok := logger.(Logger); ok { + c.logger = l + } +} + +func (c *Client) UseMetrics(metrics interface{}) { + if m, ok := metrics.(Metrics); ok { + c.metrics = m + } +} + +func (c *Client) UseTracer(tracer any) { + if tracer, ok := tracer.(trace.Tracer); ok { + c.tracer = tracer + } +} + +func (c *Client) InitMetrics() { + openaiHistogramBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10} + + c.metrics.NewHistogram( + "openai_api_request_duration", + "duration of OpenAPI requests in seconds", + openaiHistogramBuckets..., + ) + + c.metrics.NewCounter( + "openai_api_total_request_count", + "counts total number of requests made.", + ) + + c.metrics.NewCounterVec( + "openai_api_token_usage", + "counts number of tokens used.", + ) +} + +func (c *Client) AddTrace(ctx context.Context, method string) (context.Context, trace.Span) { + if c.tracer != nil { + contextWithTrace, span := c.tracer.Start(ctx, fmt.Sprintf("openai-%v", method)) + + return contextWithTrace, span + } + + return ctx, nil +} + +func (c *Client) Post(ctx context.Context, url string, input any) (response []byte, err error) { + response = make([]byte, 0) + + reqJSON, err := json.Marshal(input) + if err != nil { + c.logger.Errorf("%v", err) + return response, err + } + + resp, err := c.Call(ctx, http.MethodPost, url, bytes.NewReader(reqJSON)) + if err != nil { + c.logger.Errorf("%v", err) + return response, err + } + defer resp.Body.Close() + + response, err = io.ReadAll(resp.Body) + if err != nil { + c.logger.Errorf("%v", err) + } + + return response, err +} + +// Get makes a get request. +func (c *Client) Get(ctx context.Context, url string) (response []byte, err error) { + resp, err := c.Call(ctx, http.MethodGet, url, nil) + if err != nil { + c.logger.Errorf("%v", err) + return response, err + } + defer resp.Body.Close() + + response, err = io.ReadAll(resp.Body) + if err != nil { + c.logger.Errorf("%v", err) + } + + return response, err +} + +// Call makes a request. +func (c *Client) Call(ctx context.Context, method, endpoint string, body io.Reader) (response *http.Response, err error) { + url := c.config.BaseURL + endpoint + + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + c.logger.Errorf("%v", err) + return response, err + } + + req.Header.Add("Authorization", "Bearer "+c.config.APIKey) + req.Header.Add("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + c.logger.Errorf("%v", err) + } + + return resp, err +} diff --git a/pkg/gofr/datasource/openai/client_test.go b/pkg/gofr/datasource/openai/client_test.go new file mode 100644 index 000000000..569b749cb --- /dev/null +++ b/pkg/gofr/datasource/openai/client_test.go @@ -0,0 +1,53 @@ +package openai + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_NewClient(t *testing.T) { + tests := []struct { + name string + config *Config + httpClient *http.Client + baseURL string + expected string + expectedError error + }{ + { + name: "with default base URL", + config: &Config{APIKey: "test-key", Model: "gpt-4"}, + httpClient: &http.Client{}, + expected: "https://api.openai.com", + expectedError: nil, + }, + { + name: "with custom base URL", + config: &Config{APIKey: "test-key", Model: "gpt-4", BaseURL: "https://custom.openai.com"}, + httpClient: &http.Client{}, + expected: "https://custom.openai.com", + expectedError: nil, + }, + { + name: "missing api key", + config: &Config{Model: "gpt-4"}, + httpClient: &http.Client{}, + expectedError: ErrorMissingAPIKey, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(tt.config, WithClientHTTP(tt.httpClient)) + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + assert.Nil(t, client) + } else { + assert.Equal(t, tt.expected, client.config.BaseURL) + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/gofr/datasource/openai/go.mod b/pkg/gofr/datasource/openai/go.mod new file mode 100644 index 000000000..abe266a8b --- /dev/null +++ b/pkg/gofr/datasource/openai/go.mod @@ -0,0 +1,23 @@ +module gofr.dev/pkg/gofr/datasource/openai + +go 1.23.4 + +require ( + go.opentelemetry.io/otel v1.33.0 + go.uber.org/mock v0.5.0 +) + +require ( + github.com/google/go-querystring v1.1.0 + github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/otel/trace v1.33.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/tools v0.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/gofr/datasource/openai/go.sum b/pkg/gofr/datasource/openai/go.sum new file mode 100644 index 000000000..7e0066ae1 --- /dev/null +++ b/pkg/gofr/datasource/openai/go.sum @@ -0,0 +1,37 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/openai/openai-go v0.1.0-alpha.40 h1:97Pvd82fcstlJ/kL46tKYiGdm1XAUI4a9IJUWOMTiiU= +github.com/openai/openai-go v0.1.0-alpha.40/go.mod h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +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/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/gofr/datasource/openai/logger.go b/pkg/gofr/datasource/openai/logger.go new file mode 100644 index 000000000..8ea428b14 --- /dev/null +++ b/pkg/gofr/datasource/openai/logger.go @@ -0,0 +1,28 @@ +package openai + +type Logger interface { + Debug(args ...interface{}) + Debugf(pattern string, args ...interface{}) + Logf(pattern string, args ...interface{}) + Errorf(pattern string, args ...interface{}) +} + +type APILog struct { + ID string `json:"id,omitempty"` + Object string `json:"object,omitempty"` + Created int `json:"created,omitempty"` + Model string `json:"model,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` + SystemFingerprint string `json:"system_fingerprint,omitempty"` + Duration int64 `json:"duration,omitempty"` + + Usage struct { + PromptTokens int `json:"prompt_tokens,omitempty"` + CompletionTokens int `json:"completion_tokens,omitempty"` + TotalTokens int `json:"total_tokens,omitempty"` + CompletionTokensDetails interface{} `json:"completion_tokens_details,omitempty"` + PromptTokensDetails interface{} `json:"prompt_tokens_details,omitempty"` + } `json:"usage,omitempty"` + + Error *Error `json:"error,omitempty"` +} diff --git a/pkg/gofr/datasource/openai/metrics.go b/pkg/gofr/datasource/openai/metrics.go new file mode 100644 index 000000000..36e7eebd4 --- /dev/null +++ b/pkg/gofr/datasource/openai/metrics.go @@ -0,0 +1,13 @@ +package openai + +import "context" + +type Metrics interface { + NewHistogram(name, desc string, buckets ...float64) + NewCounter(name, desc string, labels ...string) + NewCounterVec(name, desc string, labels ...string) + + RecordHistogram(ctx context.Context, name string, value float64, labels ...string) + RecordRequestCount(ctx context.Context, name string, labels ...string) + RecordTokenUsage(ctx context.Context, name string, promptTokens, completionTokens int, labels ...string) +} diff --git a/pkg/gofr/datasource/openai/mock_logger.go b/pkg/gofr/datasource/openai/mock_logger.go new file mode 100644 index 000000000..e1dcbc1e9 --- /dev/null +++ b/pkg/gofr/datasource/openai/mock_logger.go @@ -0,0 +1,107 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: logger.go +// +// Generated by this command: +// +// mockgen -source=logger.go -destination=mock_logger.go -package=openai +// + +// Package openai is a generated GoMock package. +package openai + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder + isgomock struct{} +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +// Debug mocks base method. +func (m *MockLogger) Debug(args ...any) { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...) +} + +// Debugf mocks base method. +func (m *MockLogger) Debugf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + +// Errorf mocks base method. +func (m *MockLogger) Errorf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Errorf", varargs...) +} + +// Errorf indicates an expected call of Errorf. +func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...) +} + +// Logf mocks base method. +func (m *MockLogger) Logf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Logf", varargs...) +} + +// Logf indicates an expected call of Logf. +func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...) +} diff --git a/pkg/gofr/datasource/openai/mock_metrics.go b/pkg/gofr/datasource/openai/mock_metrics.go new file mode 100644 index 000000000..b868f541f --- /dev/null +++ b/pkg/gofr/datasource/openai/mock_metrics.go @@ -0,0 +1,143 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: metrics.go +// +// Generated by this command: +// +// mockgen -source=metrics.go -destination=mock_metrics.go -package=openai +// + +// Package openai is a generated GoMock package. +package openai + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockMetrics is a mock of Metrics interface. +type MockMetrics struct { + ctrl *gomock.Controller + recorder *MockMetricsMockRecorder + isgomock struct{} +} + +// MockMetricsMockRecorder is the mock recorder for MockMetrics. +type MockMetricsMockRecorder struct { + mock *MockMetrics +} + +// NewMockMetrics creates a new mock instance. +func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics { + mock := &MockMetrics{ctrl: ctrl} + mock.recorder = &MockMetricsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder { + return m.recorder +} + +// NewCounter mocks base method. +func (m *MockMetrics) NewCounter(name, desc string, labels ...string) { + m.ctrl.T.Helper() + varargs := []any{name, desc} + for _, a := range labels { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "NewCounter", varargs...) +} + +// NewCounter indicates an expected call of NewCounter. +func (mr *MockMetricsMockRecorder) NewCounter(name, desc any, labels ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{name, desc}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCounter", reflect.TypeOf((*MockMetrics)(nil).NewCounter), varargs...) +} + +// NewCounterVec mocks base method. +func (m *MockMetrics) NewCounterVec(name, desc string, labels ...string) { + m.ctrl.T.Helper() + varargs := []any{name, desc} + for _, a := range labels { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "NewCounterVec", varargs...) +} + +// NewCounterVec indicates an expected call of NewCounterVec. +func (mr *MockMetricsMockRecorder) NewCounterVec(name, desc any, labels ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{name, desc}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCounterVec", reflect.TypeOf((*MockMetrics)(nil).NewCounterVec), varargs...) +} + +// NewHistogram mocks base method. +func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) { + m.ctrl.T.Helper() + varargs := []any{name, desc} + for _, a := range buckets { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "NewHistogram", varargs...) +} + +// NewHistogram indicates an expected call of NewHistogram. +func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{name, desc}, buckets...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...) +} + +// RecordHistogram mocks base method. +func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) { + m.ctrl.T.Helper() + varargs := []any{ctx, name, value} + for _, a := range labels { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "RecordHistogram", varargs...) +} + +// RecordHistogram indicates an expected call of RecordHistogram. +func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, name, value}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...) +} + +// RecordRequestCount mocks base method. +func (m *MockMetrics) RecordRequestCount(ctx context.Context, name string, labels ...string) { + m.ctrl.T.Helper() + varargs := []any{ctx, name} + for _, a := range labels { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "RecordRequestCount", varargs...) +} + +// RecordRequestCount indicates an expected call of RecordRequestCount. +func (mr *MockMetricsMockRecorder) RecordRequestCount(ctx, name any, labels ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, name}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordRequestCount", reflect.TypeOf((*MockMetrics)(nil).RecordRequestCount), varargs...) +} + +// RecordTokenUsage mocks base method. +func (m *MockMetrics) RecordTokenUsage(ctx context.Context, name string, promptTokens, completionTokens int, labels ...string) { + m.ctrl.T.Helper() + varargs := []any{ctx, name, promptTokens, completionTokens} + for _, a := range labels { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "RecordTokenUsage", varargs...) +} + +// RecordTokenUsage indicates an expected call of RecordTokenUsage. +func (mr *MockMetricsMockRecorder) RecordTokenUsage(ctx, name, promptTokens, completionTokens any, labels ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, name, promptTokens, completionTokens}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordTokenUsage", reflect.TypeOf((*MockMetrics)(nil).RecordTokenUsage), varargs...) +}