From aa48ff485e8f03b263ee73da748dc92c9f7fc527 Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Tue, 24 Dec 2024 11:58:41 +0530 Subject: [PATCH 01/17] wrapped openai api chat completion endpoint --- pkg/gofr/datasource/openai/chatcompletion.go | 184 ++++++++++++++++++ .../datasource/openai/chatcompletion_test.go | 146 ++++++++++++++ pkg/gofr/datasource/openai/client.go | 143 ++++++++++++++ pkg/gofr/datasource/openai/client_test.go | 36 ++++ pkg/gofr/datasource/openai/go.mod | 23 +++ pkg/gofr/datasource/openai/go.sum | 37 ++++ pkg/gofr/datasource/openai/logger.go | 28 +++ pkg/gofr/datasource/openai/metrics.go | 13 ++ pkg/gofr/datasource/openai/mock_logger.go | 107 ++++++++++ pkg/gofr/datasource/openai/mock_metrics.go | 143 ++++++++++++++ 10 files changed, 860 insertions(+) create mode 100644 pkg/gofr/datasource/openai/chatcompletion.go create mode 100644 pkg/gofr/datasource/openai/chatcompletion_test.go create mode 100644 pkg/gofr/datasource/openai/client.go create mode 100644 pkg/gofr/datasource/openai/client_test.go create mode 100644 pkg/gofr/datasource/openai/go.mod create mode 100644 pkg/gofr/datasource/openai/go.sum create mode 100644 pkg/gofr/datasource/openai/logger.go create mode 100644 pkg/gofr/datasource/openai/metrics.go create mode 100644 pkg/gofr/datasource/openai/mock_logger.go create mode 100644 pkg/gofr/datasource/openai/mock_metrics.go diff --git a/pkg/gofr/datasource/openai/chatcompletion.go b/pkg/gofr/datasource/openai/chatcompletion.go new file mode 100644 index 000000000..23cc18c3f --- /dev/null +++ b/pkg/gofr/datasource/openai/chatcompletion.go @@ -0,0 +1,184 @@ +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"` + + ResposneFormat 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:"toolChoice,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 struct { + PromptTokens int `json:"prompt_tokens,omitempty"` + CompletionTokens int `json:"completion_tokens,omitempty"` + TotalTokens int `json:"total_tokens,omitempty"` + CompletionTokelDetails interface{} `json:"completion_tokens_details,omitempty"` + PromptTokenDetails interface{} `json:"prompt_tokens_details,omitempty"` + } `json:"usage,omitempty"` + + Error *Error `json:"error,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) + + ql := &OpenAiAPILog{ + 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(ql, startTime, "ChatCompletion", span) + + return response, err +} + +func (c *Client) SendChatCompletionOperationStats(ql *OpenAiAPILog, startTime time.Time, method string, span trace.Span) { + duration := time.Since(startTime).Microseconds() + + ql.Duration = duration + + c.logger.Debug(ql) + + c.metrics.RecordHistogram(context.Background(), "openai_api_request_duration", float64(duration)) + c.metrics.RecordRequestCount(context.Background(), "openai_api_total_request_count") + c.metrics.RecordTokenUsage(context.Background(), "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..4597d691f --- /dev/null +++ b/pkg/gofr/datasource/openai/chatcompletion_test.go @@ -0,0 +1,146 @@ +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: struct { + PromptTokens int `json:"prompt_tokens,omitempty"` + CompletionTokens int `json:"completion_tokens,omitempty"` + TotalTokens int `json:"total_tokens,omitempty"` + CompletionTokelDetails interface{} `json:"completion_tokens_details,omitempty"` + PromptTokenDetails interface{} `json:"prompt_tokens_details,omitempty"` + }{ + 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, + }, + logger: mockLogger, + metrics: mockMetrics, + } + + tt.setupMocks(mockLogger, mockMetrics) + + response, err := client.CreateCompletions(context.Background(), tt.request) + + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + 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..0c9c1d47e --- /dev/null +++ b/pkg/gofr/datasource/openai/client.go @@ -0,0 +1,143 @@ +package openai + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/google/go-querystring/query" + "go.opentelemetry.io/otel/trace" +) + +type Config struct { + APIKey string + Model string + BaseURL string +} + +type Client struct { + config *Config + logger Logger + metrics Metrics + tracer trace.Tracer +} + +func NewCLient(config *Config) *Client { + if config.BaseURL == "" { + config.BaseURL = "https://api.openai.com" + } + + return &Client{ + config: config, + } +} + +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 { + return response, err + } + + resp, err := c.Call(ctx, http.MethodPost, url, bytes.NewReader(reqJSON)) + if err != nil { + return response, err + } + defer resp.Body.Close() + + response, err = io.ReadAll(resp.Body) + + return response, err +} + +// Get makes a get request. +func (c *Client) Get(ctx context.Context, url string, input any) (response []byte, err error) { + if input != nil { + vals, _ := query.Values(input) + queryString := vals.Encode() + + if queryString != "" { + url += "?" + queryString + } + } + + resp, err := c.Call(ctx, http.MethodGet, url, nil) + if err != nil { + return response, err + } + defer resp.Body.Close() + + response, err = io.ReadAll(resp.Body) + + 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 { + return response, err + } + + req.Header.Add("Authorization", "Bearer "+c.config.APIKey) + req.Header.Add("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + + 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..e7cbb4551 --- /dev/null +++ b/pkg/gofr/datasource/openai/client_test.go @@ -0,0 +1,36 @@ +package openai + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_NewClient(t *testing.T) { + tests := []struct { + name string + config *Config + baseURL string + expected string + }{ + { + name: "with default base URL", + config: &Config{APIKey: "test-key", Model: "gpt-4"}, + expected: "https://api.openai.com", + }, + { + name: "with custom base URL", + config: &Config{APIKey: "test-key", Model: "gpt-4", BaseURL: "https://custom.openai.com"}, + expected: "https://custom.openai.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := NewCLient(tt.config) + assert.NotNil(t, client) + assert.Equal(t, tt.expected, client.config.BaseURL) + }) + } +} + 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..9201e73c3 --- /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 OpenAiAPILog 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"` + CompletionTokelDetails interface{} `json:"completion_tokens_details,omitempty"` + PromptTokenDetails interface{} `json:"prompt_tokens_details,omitempty"` + } `json:"usage,omitempty"` + + Error *Error `json:"error,omitempty"` +} \ No newline at end of file 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...) +} From 251ca2a73e2ac40208c786558747599f0e39bfc3 Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Tue, 24 Dec 2024 12:01:53 +0530 Subject: [PATCH 02/17] formatting changes --- pkg/gofr/datasource/openai/client_test.go | 1 - pkg/gofr/datasource/openai/logger.go | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/gofr/datasource/openai/client_test.go b/pkg/gofr/datasource/openai/client_test.go index e7cbb4551..e866b03c1 100644 --- a/pkg/gofr/datasource/openai/client_test.go +++ b/pkg/gofr/datasource/openai/client_test.go @@ -33,4 +33,3 @@ func Test_NewClient(t *testing.T) { }) } } - diff --git a/pkg/gofr/datasource/openai/logger.go b/pkg/gofr/datasource/openai/logger.go index 9201e73c3..cfec5d506 100644 --- a/pkg/gofr/datasource/openai/logger.go +++ b/pkg/gofr/datasource/openai/logger.go @@ -14,7 +14,7 @@ type OpenAiAPILog struct { Model string `json:"model,omitempty"` ServiceTier string `json:"service_tier,omitempty"` SystemFingerprint string `json:"system_fingerprint,omitempty"` - Duration int64 `json:"duration,omitempty"` + Duration int64 `json:"duration,omitempty"` Usage struct { PromptTokens int `json:"prompt_tokens,omitempty"` @@ -25,4 +25,4 @@ type OpenAiAPILog struct { } `json:"usage,omitempty"` Error *Error `json:"error,omitempty"` -} \ No newline at end of file +} From 1db7000c1b4e8f6b6b3aad672ce3cf42720437cf Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Tue, 7 Jan 2025 22:07:51 +0530 Subject: [PATCH 03/17] increased code coverage --- .../datasource/openai/chatcompletion_test.go | 5 +- pkg/gofr/datasource/openai/client.go | 55 +++++++++++++++---- pkg/gofr/datasource/openai/client_test.go | 26 +++++---- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/pkg/gofr/datasource/openai/chatcompletion_test.go b/pkg/gofr/datasource/openai/chatcompletion_test.go index 4597d691f..64b02453b 100644 --- a/pkg/gofr/datasource/openai/chatcompletion_test.go +++ b/pkg/gofr/datasource/openai/chatcompletion_test.go @@ -104,8 +104,9 @@ func Test_ChatCompletions(t *testing.T) { APIKey: "test-api-key", BaseURL: serverURL, }, - logger: mockLogger, - metrics: mockMetrics, + httpClient: http.DefaultClient, + logger: mockLogger, + metrics: mockMetrics, } tt.setupMocks(mockLogger, mockMetrics) diff --git a/pkg/gofr/datasource/openai/client.go b/pkg/gofr/datasource/openai/client.go index 0c9c1d47e..e39ec9604 100644 --- a/pkg/gofr/datasource/openai/client.go +++ b/pkg/gofr/datasource/openai/client.go @@ -7,31 +7,52 @@ import ( "fmt" "io" "net/http" + "time" "github.com/google/go-querystring/query" "go.opentelemetry.io/otel/trace" ) type Config struct { - APIKey string - Model string - BaseURL string + APIKey string + Model string + BaseURL string + Timeout time.Duration + MaxIdleConns int } type Client struct { - config *Config - logger Logger - metrics Metrics - tracer trace.Tracer + config *Config + logger Logger + metrics Metrics + tracer trace.Tracer + httpClient *http.Client } -func NewCLient(config *Config) *Client { +func NewClient(config *Config, customHTTPClient *http.Client) *Client { if config.BaseURL == "" { config.BaseURL = "https://api.openai.com" } + // Use the provided HTTP client or create a new one with defaults + var httpClient *http.Client + + if customHTTPClient != nil { + httpClient = customHTTPClient + } else { + // Create a new HTTP client with default or configured settings + httpClient = &http.Client{ + Timeout: config.Timeout, + Transport: &http.Transport{ + MaxIdleConns: config.MaxIdleConns, + IdleConnTimeout: 30 * time.Second, + }, + } + } + return &Client{ - config: config, + config: config, + httpClient: httpClient, } } @@ -88,16 +109,21 @@ func (c *Client) Post(ctx context.Context, url string, input any) (response []by 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 } @@ -115,11 +141,15 @@ func (c *Client) Get(ctx context.Context, url string, input any) (response []byt 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 } @@ -130,14 +160,17 @@ func (c *Client) Call(ctx context.Context, method, endpoint string, body io.Read 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") - client := &http.Client{} - resp, err := client.Do(req) + 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 index e866b03c1..21da11fe2 100644 --- a/pkg/gofr/datasource/openai/client_test.go +++ b/pkg/gofr/datasource/openai/client_test.go @@ -1,6 +1,7 @@ package openai import ( + "net/http" "testing" "github.com/stretchr/testify/assert" @@ -8,26 +9,29 @@ import ( func Test_NewClient(t *testing.T) { tests := []struct { - name string - config *Config - baseURL string - expected string + name string + config *Config + httpClient *http.Client + baseURL string + expected string }{ { - name: "with default base URL", - config: &Config{APIKey: "test-key", Model: "gpt-4"}, - expected: "https://api.openai.com", + name: "with default base URL", + config: &Config{APIKey: "test-key", Model: "gpt-4"}, + httpClient: &http.Client{}, + expected: "https://api.openai.com", }, { - name: "with custom base URL", - config: &Config{APIKey: "test-key", Model: "gpt-4", BaseURL: "https://custom.openai.com"}, - expected: "https://custom.openai.com", + 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", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client := NewCLient(tt.config) + client := NewClient(tt.config, tt.httpClient) assert.NotNil(t, client) assert.Equal(t, tt.expected, client.config.BaseURL) }) From bc4ce3c917c2c4404ea64f9f759daacf8611bffa Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Wed, 8 Jan 2025 22:30:54 +0530 Subject: [PATCH 04/17] resolved changes --- pkg/gofr/datasource/openai/chatcompletion.go | 13 +++--- .../datasource/openai/chatcompletion_test.go | 10 ++--- pkg/gofr/datasource/openai/client.go | 44 ++++++++++++++----- pkg/gofr/datasource/openai/client_test.go | 38 +++++++++++----- pkg/gofr/datasource/openai/logger.go | 12 ++--- 5 files changed, 77 insertions(+), 40 deletions(-) diff --git a/pkg/gofr/datasource/openai/chatcompletion.go b/pkg/gofr/datasource/openai/chatcompletion.go index 23cc18c3f..b9cb0edc5 100644 --- a/pkg/gofr/datasource/openai/chatcompletion.go +++ b/pkg/gofr/datasource/openai/chatcompletion.go @@ -58,7 +58,7 @@ type CreateCompletionsRequest struct { } `json:"function,omitempty"` } `json:"tools,omitempty"` - ToolChoice interface{} `json:"toolChoice,omitempty"` + ToolChoice interface{} `json:"tool_choice,omitempty"` ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"` Suffix string `json:"suffix,omitempty"` User string `json:"user,omitempty"` @@ -96,8 +96,8 @@ type CreateCompletionsResponse struct { PromptTokens int `json:"prompt_tokens,omitempty"` CompletionTokens int `json:"completion_tokens,omitempty"` TotalTokens int `json:"total_tokens,omitempty"` - CompletionTokelDetails interface{} `json:"completion_tokens_details,omitempty"` - PromptTokenDetails interface{} `json:"prompt_tokens_details,omitempty"` + CompletionTokensDetails interface{} `json:"completion_tokens_details,omitempty"` + PromptTokensDetails interface{} `json:"prompt_tokens_details,omitempty"` } `json:"usage,omitempty"` Error *Error `json:"error,omitempty"` @@ -149,8 +149,11 @@ func (c *Client) CreateCompletions(ctx context.Context, r *CreateCompletionsRequ } err = json.Unmarshal(raw, &response) + if err != nil { + return nil, err + } - ql := &OpenAiAPILog{ + ql := &APILog{ ID: response.ID, Object: response.Object, Created: response.Created, @@ -166,7 +169,7 @@ func (c *Client) CreateCompletions(ctx context.Context, r *CreateCompletionsRequ return response, err } -func (c *Client) SendChatCompletionOperationStats(ql *OpenAiAPILog, startTime time.Time, method string, span trace.Span) { +func (c *Client) SendChatCompletionOperationStats(ql *APILog, startTime time.Time, method string, span trace.Span) { duration := time.Since(startTime).Microseconds() ql.Duration = duration diff --git a/pkg/gofr/datasource/openai/chatcompletion_test.go b/pkg/gofr/datasource/openai/chatcompletion_test.go index 64b02453b..e977cba83 100644 --- a/pkg/gofr/datasource/openai/chatcompletion_test.go +++ b/pkg/gofr/datasource/openai/chatcompletion_test.go @@ -38,11 +38,11 @@ func Test_ChatCompletions(t *testing.T) { Object: "chat.completion", Created: 1234567890, Usage: struct { - PromptTokens int `json:"prompt_tokens,omitempty"` - CompletionTokens int `json:"completion_tokens,omitempty"` - TotalTokens int `json:"total_tokens,omitempty"` - CompletionTokelDetails interface{} `json:"completion_tokens_details,omitempty"` - PromptTokenDetails interface{} `json:"prompt_tokens_details,omitempty"` + 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"` }{ PromptTokens: 10, CompletionTokens: 20, diff --git a/pkg/gofr/datasource/openai/client.go b/pkg/gofr/datasource/openai/client.go index e39ec9604..afd24d4f1 100644 --- a/pkg/gofr/datasource/openai/client.go +++ b/pkg/gofr/datasource/openai/client.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -29,31 +30,50 @@ type Client struct { httpClient *http.Client } -func NewClient(config *Config, customHTTPClient *http.Client) *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 - var httpClient *http.Client - - if customHTTPClient != nil { - httpClient = customHTTPClient - } else { - // Create a new HTTP client with default or configured settings - httpClient = &http.Client{ + c := &Client{ + config: config, + httpClient: &http.Client{ Timeout: config.Timeout, Transport: &http.Transport{ MaxIdleConns: config.MaxIdleConns, IdleConnTimeout: 30 * time.Second, }, - } + }, } - return &Client{ - config: config, - httpClient: httpClient, + for _, opt := range opts { + opt(c) } + + return c, nil } func (c *Client) UseLogger(logger interface{}) { diff --git a/pkg/gofr/datasource/openai/client_test.go b/pkg/gofr/datasource/openai/client_test.go index 21da11fe2..deef89aa2 100644 --- a/pkg/gofr/datasource/openai/client_test.go +++ b/pkg/gofr/datasource/openai/client_test.go @@ -9,31 +9,45 @@ import ( func Test_NewClient(t *testing.T) { tests := []struct { - name string - config *Config - httpClient *http.Client - baseURL string - expected string + 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", + 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 := NewClient(tt.config, tt.httpClient) - assert.NotNil(t, client) - assert.Equal(t, tt.expected, client.config.BaseURL) + 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/logger.go b/pkg/gofr/datasource/openai/logger.go index cfec5d506..8ea428b14 100644 --- a/pkg/gofr/datasource/openai/logger.go +++ b/pkg/gofr/datasource/openai/logger.go @@ -7,7 +7,7 @@ type Logger interface { Errorf(pattern string, args ...interface{}) } -type OpenAiAPILog struct { +type APILog struct { ID string `json:"id,omitempty"` Object string `json:"object,omitempty"` Created int `json:"created,omitempty"` @@ -17,11 +17,11 @@ type OpenAiAPILog struct { 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"` - CompletionTokelDetails interface{} `json:"completion_tokens_details,omitempty"` - PromptTokenDetails interface{} `json:"prompt_tokens_details,omitempty"` + 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"` From 63e2e5df624fab7205d0afc6ec22911538a1d1a7 Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Thu, 9 Jan 2025 15:34:55 +0530 Subject: [PATCH 05/17] added usage struct to package level, updated get method, and resolved other minor changes --- pkg/gofr/datasource/openai/chatcompletion.go | 26 ++++++++++--------- .../datasource/openai/chatcompletion_test.go | 12 +++------ pkg/gofr/datasource/openai/client.go | 14 ++-------- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/pkg/gofr/datasource/openai/chatcompletion.go b/pkg/gofr/datasource/openai/chatcompletion.go index b9cb0edc5..4be557a85 100644 --- a/pkg/gofr/datasource/openai/chatcompletion.go +++ b/pkg/gofr/datasource/openai/chatcompletion.go @@ -92,17 +92,19 @@ type CreateCompletionsResponse struct { FinishReason string `json:"finish_reason,omitempty"` } `json:"choices,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"` + 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"` @@ -164,21 +166,21 @@ func (c *Client) CreateCompletions(ctx context.Context, r *CreateCompletionsRequ Error: response.Error, } - c.SendChatCompletionOperationStats(ql, startTime, "ChatCompletion", span) + c.SendChatCompletionOperationStats(ctx, ql, startTime, "ChatCompletion", span) return response, err } -func (c *Client) SendChatCompletionOperationStats(ql *APILog, startTime time.Time, method string, span trace.Span) { +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(context.Background(), "openai_api_request_duration", float64(duration)) - c.metrics.RecordRequestCount(context.Background(), "openai_api_total_request_count") - c.metrics.RecordTokenUsage(context.Background(), "openai_api_token_usage", ql.Usage.PromptTokens, ql.Usage.CompletionTokens) + 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() diff --git a/pkg/gofr/datasource/openai/chatcompletion_test.go b/pkg/gofr/datasource/openai/chatcompletion_test.go index e977cba83..e75d95ef7 100644 --- a/pkg/gofr/datasource/openai/chatcompletion_test.go +++ b/pkg/gofr/datasource/openai/chatcompletion_test.go @@ -37,13 +37,7 @@ func Test_ChatCompletions(t *testing.T) { ID: "test-id", Object: "chat.completion", Created: 1234567890, - 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"` - }{ + Usage: Usage{ PromptTokens: 10, CompletionTokens: 20, TotalTokens: 30, @@ -114,7 +108,7 @@ func Test_ChatCompletions(t *testing.T) { response, err := client.CreateCompletions(context.Background(), tt.request) if tt.expectedError != nil { - assert.Equal(t, tt.expectedError, err) + require.ErrorIs(t, err, tt.expectedError) assert.Nil(t, response) } else { require.NoError(t, err) @@ -136,7 +130,7 @@ func setupTestServer(t *testing.T, path string, response interface{}) *httptest. w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(response) - + if err != nil { t.Error(err) return diff --git a/pkg/gofr/datasource/openai/client.go b/pkg/gofr/datasource/openai/client.go index afd24d4f1..d4db3c7c4 100644 --- a/pkg/gofr/datasource/openai/client.go +++ b/pkg/gofr/datasource/openai/client.go @@ -10,7 +10,6 @@ import ( "net/http" "time" - "github.com/google/go-querystring/query" "go.opentelemetry.io/otel/trace" ) @@ -99,7 +98,7 @@ func (c *Client) InitMetrics() { c.metrics.NewHistogram( "openai_api_request_duration", - "Duration of OpenAPI requests in seconds", + "duration of OpenAPI requests in seconds", openaiHistogramBuckets..., ) @@ -149,16 +148,7 @@ func (c *Client) Post(ctx context.Context, url string, input any) (response []by } // Get makes a get request. -func (c *Client) Get(ctx context.Context, url string, input any) (response []byte, err error) { - if input != nil { - vals, _ := query.Values(input) - queryString := vals.Encode() - - if queryString != "" { - url += "?" + queryString - } - } - +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) From 2d869c3e1760482a31b0a5b8780a13edfe14331b Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Thu, 9 Jan 2025 15:47:25 +0530 Subject: [PATCH 06/17] formatting changes --- pkg/gofr/datasource/openai/chatcompletion_test.go | 2 +- pkg/gofr/datasource/openai/client_test.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/gofr/datasource/openai/chatcompletion_test.go b/pkg/gofr/datasource/openai/chatcompletion_test.go index e75d95ef7..20e1cdd90 100644 --- a/pkg/gofr/datasource/openai/chatcompletion_test.go +++ b/pkg/gofr/datasource/openai/chatcompletion_test.go @@ -130,7 +130,7 @@ func setupTestServer(t *testing.T, path string, response interface{}) *httptest. w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(response) - + if err != nil { t.Error(err) return diff --git a/pkg/gofr/datasource/openai/client_test.go b/pkg/gofr/datasource/openai/client_test.go index deef89aa2..569b749cb 100644 --- a/pkg/gofr/datasource/openai/client_test.go +++ b/pkg/gofr/datasource/openai/client_test.go @@ -24,16 +24,16 @@ func Test_NewClient(t *testing.T) { 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", + 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{}, + name: "missing api key", + config: &Config{Model: "gpt-4"}, + httpClient: &http.Client{}, expectedError: ErrorMissingAPIKey, }, } From 71fa8391e61f6758489b2e96bfbfa4c064ea2730 Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Fri, 10 Jan 2025 13:58:17 +0530 Subject: [PATCH 07/17] typo fix --- pkg/gofr/datasource/openai/chatcompletion.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/datasource/openai/chatcompletion.go b/pkg/gofr/datasource/openai/chatcompletion.go index 4be557a85..074821e99 100644 --- a/pkg/gofr/datasource/openai/chatcompletion.go +++ b/pkg/gofr/datasource/openai/chatcompletion.go @@ -35,7 +35,7 @@ type CreateCompletionsRequest struct { Format string `json:"format,omitempty"` } `json:"audio,omitempty"` - ResposneFormat interface{} `json:"response_format,omitempty"` + ResponseFormat interface{} `json:"response_format,omitempty"` Seed int `json:"seed,omitempty"` ServiceTier string `json:"service_tier,omitempty"` Stop interface{} `json:"stop,omitempty"` From 4f47c74cb9b425832fe8f8aa0d98016620dbee75 Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Mon, 13 Jan 2025 19:11:27 +0530 Subject: [PATCH 08/17] resolved changes, added more tests to client, increased code coverage --- pkg/gofr/datasource/openai/client_test.go | 53 -- .../openai/chatcompletion.go | 18 +- .../openai/chatcompletion_test.go | 30 +- .../{datasource => service}/openai/client.go | 8 +- pkg/gofr/service/openai/client_test.go | 460 ++++++++++++++++++ .../{datasource => service}/openai/go.mod | 0 .../{datasource => service}/openai/go.sum | 0 .../{datasource => service}/openai/logger.go | 0 .../{datasource => service}/openai/metrics.go | 0 .../openai/mock_logger.go | 0 .../openai/mock_metrics.go | 0 11 files changed, 490 insertions(+), 79 deletions(-) delete mode 100644 pkg/gofr/datasource/openai/client_test.go rename pkg/gofr/{datasource => service}/openai/chatcompletion.go (93%) rename pkg/gofr/{datasource => service}/openai/chatcompletion_test.go (85%) rename pkg/gofr/{datasource => service}/openai/client.go (96%) create mode 100644 pkg/gofr/service/openai/client_test.go rename pkg/gofr/{datasource => service}/openai/go.mod (100%) rename pkg/gofr/{datasource => service}/openai/go.sum (100%) rename pkg/gofr/{datasource => service}/openai/logger.go (100%) rename pkg/gofr/{datasource => service}/openai/metrics.go (100%) rename pkg/gofr/{datasource => service}/openai/mock_logger.go (100%) rename pkg/gofr/{datasource => service}/openai/mock_metrics.go (100%) diff --git a/pkg/gofr/datasource/openai/client_test.go b/pkg/gofr/datasource/openai/client_test.go deleted file mode 100644 index 569b749cb..000000000 --- a/pkg/gofr/datasource/openai/client_test.go +++ /dev/null @@ -1,53 +0,0 @@ -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/chatcompletion.go b/pkg/gofr/service/openai/chatcompletion.go similarity index 93% rename from pkg/gofr/datasource/openai/chatcompletion.go rename to pkg/gofr/service/openai/chatcompletion.go index 074821e99..ca94810b4 100644 --- a/pkg/gofr/datasource/openai/chatcompletion.go +++ b/pkg/gofr/service/openai/chatcompletion.go @@ -113,9 +113,9 @@ type Error struct { } 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") + 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 { @@ -131,18 +131,18 @@ func (c *Client) CreateCompletions(ctx context.Context, r *CreateCompletionsRequ startTime := time.Now() if r.Messages == nil && r.Model == "" { - c.logger.Errorf("%v", ErrMissingBoth) - return nil, ErrMissingBoth + c.logger.Errorf("%v", errMissingBoth) + return nil, errMissingBoth } if r.Messages == nil { - c.logger.Errorf("%v", ErrMissingMessages) - return nil, ErrMissingMessages + c.logger.Errorf("%v", errMissingMessages) + return nil, errMissingMessages } if r.Model == "" { - c.logger.Errorf("%v", ErrMissingModel) - return nil, ErrMissingModel + c.logger.Errorf("%v", errMissingModel) + return nil, errMissingModel } raw, err := c.CreateCompletionsRaw(tracerCtx, r) diff --git a/pkg/gofr/datasource/openai/chatcompletion_test.go b/pkg/gofr/service/openai/chatcompletion_test.go similarity index 85% rename from pkg/gofr/datasource/openai/chatcompletion_test.go rename to pkg/gofr/service/openai/chatcompletion_test.go index 20e1cdd90..e9ae215ef 100644 --- a/pkg/gofr/datasource/openai/chatcompletion_test.go +++ b/pkg/gofr/service/openai/chatcompletion_test.go @@ -12,7 +12,14 @@ import ( "go.uber.org/mock/gomock" ) -//nolint:funlen // Function length is intentional due to complexity +type test struct { + name string + request *CreateCompletionsRequest + response *CreateCompletionsResponse + expectedError error + setupMocks func(*MockLogger, *MockMetrics) +} + func Test_ChatCompletions(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -20,13 +27,7 @@ func Test_ChatCompletions(t *testing.T) { mockLogger := NewMockLogger(ctrl) mockMetrics := NewMockMetrics(ctrl) - tests := []struct { - name string - request *CreateCompletionsRequest - response *CreateCompletionsResponse - expectedError error - setupMocks func(*MockLogger, *MockMetrics) - }{ + tests := []test{ { name: "successful completion request", request: &CreateCompletionsRequest{ @@ -54,9 +55,9 @@ func Test_ChatCompletions(t *testing.T) { { name: "missing both messages and model", request: &CreateCompletionsRequest{}, - expectedError: ErrMissingBoth, + expectedError: errMissingBoth, setupMocks: func(logger *MockLogger, _ *MockMetrics) { - logger.EXPECT().Errorf("%v", ErrMissingBoth) + logger.EXPECT().Errorf("%v", errMissingBoth) }, }, { @@ -64,9 +65,9 @@ func Test_ChatCompletions(t *testing.T) { request: &CreateCompletionsRequest{ Model: "gpt-3.5-turbo", }, - expectedError: ErrMissingMessages, + expectedError: errMissingMessages, setupMocks: func(logger *MockLogger, _ *MockMetrics) { - logger.EXPECT().Errorf("%v", ErrMissingMessages) + logger.EXPECT().Errorf("%v", errMissingMessages) }, }, { @@ -74,9 +75,9 @@ func Test_ChatCompletions(t *testing.T) { request: &CreateCompletionsRequest{ Messages: []Message{{Role: "user", Content: "Hello"}}, }, - expectedError: ErrMissingModel, + expectedError: errMissingModel, setupMocks: func(logger *MockLogger, _ *MockMetrics) { - logger.EXPECT().Errorf("%v", ErrMissingModel) + logger.EXPECT().Errorf("%v", errMissingModel) }, }, } @@ -104,7 +105,6 @@ func Test_ChatCompletions(t *testing.T) { } tt.setupMocks(mockLogger, mockMetrics) - response, err := client.CreateCompletions(context.Background(), tt.request) if tt.expectedError != nil { diff --git a/pkg/gofr/datasource/openai/client.go b/pkg/gofr/service/openai/client.go similarity index 96% rename from pkg/gofr/datasource/openai/client.go rename to pkg/gofr/service/openai/client.go index d4db3c7c4..688926836 100644 --- a/pkg/gofr/datasource/openai/client.go +++ b/pkg/gofr/service/openai/client.go @@ -30,7 +30,7 @@ type Client struct { } var ( - ErrorMissingAPIKey = errors.New("api key not provided") + errorMissingAPIKey = errors.New("api key not provided") ) type ClientOption func(*Client) @@ -49,13 +49,17 @@ func WithClientTimeout(d time.Duration) func(*Client) { func NewClient(config *Config, opts ...ClientOption) (*Client, error) { if config.APIKey == "" { - return nil, ErrorMissingAPIKey + return nil, errorMissingAPIKey } if config.BaseURL == "" { config.BaseURL = "https://api.openai.com" } + if config.Model == "" { + config.Model = "gpt-4o" + } + // Use the provided HTTP client or create a new one with defaults c := &Client{ config: config, diff --git a/pkg/gofr/service/openai/client_test.go b/pkg/gofr/service/openai/client_test.go new file mode 100644 index 000000000..c14d2182f --- /dev/null +++ b/pkg/gofr/service/openai/client_test.go @@ -0,0 +1,460 @@ +package openai + +import ( + "context" + "errors" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" + "go.uber.org/mock/gomock" +) + +var ( + errMockRead = errors.New("read error") + errNetworkError = errors.New("network error") +) + +func Test_NewClient(t *testing.T) { + tests := []struct { + name string + config *Config + opts []ClientOption + baseURL string + expected string + timeout time.Duration + expectedError error + }{ + { + name: "with default base URL", + config: &Config{APIKey: "test-key", Model: "gpt-4"}, + opts: []ClientOption{WithClientHTTP(&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"}, + opts: []ClientOption{WithClientHTTP(&http.Client{})}, + expected: "https://custom.openai.com", + expectedError: nil, + }, + { + name: "missing api key", + config: &Config{Model: "gpt-4"}, + opts: []ClientOption{WithClientHTTP(&http.Client{})}, + expectedError: errorMissingAPIKey, + }, + { + name: "with custom timeout", + config: &Config{APIKey: "test-key", Model: "gpt-4"}, + opts: []ClientOption{WithClientTimeout(5 * time.Second)}, + expected: "https://api.openai.com", + timeout: 5 * time.Second, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(tt.config, tt.opts...) + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + assert.Nil(t, client) + } else { + assert.Equal(t, tt.expected, client.config.BaseURL) + + if tt.timeout > 0 { + assert.Equal(t, tt.timeout, client.httpClient.Timeout) + } + + require.NoError(t, err) + } + }) + } +} + +func Test_UseLogger(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLogger(ctrl) + + config := &Config{ + APIKey: "key", + } + + client, _ := NewClient(config) + client.UseLogger(mockLogger) + + assert.NotNil(t, client.logger) +} + +func Test_UseMetrics(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMetrics := NewMockMetrics(ctrl) + + config := &Config{ + APIKey: "key", + } + + client, _ := NewClient(config) + client.UseMetrics(mockMetrics) + + assert.NotNil(t, client.metrics) +} + +func Test_UseTracer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tracer := trace.NewNoopTracerProvider().Tracer("test-tracer") + + config := &Config{ + APIKey: "key", + } + + client, _ := NewClient(config) + client.UseTracer(tracer) + + assert.NotNil(t, client.tracer) +} + +func Test_InitMetrics(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMetrics := NewMockMetrics(ctrl) + + config := &Config{ + APIKey: "key", + } + + client, _ := NewClient(config) + client.UseMetrics(mockMetrics) + + openaiHistogramBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10} + + mockMetrics.EXPECT().NewHistogram( + "openai_api_request_duration", + "duration of OpenAPI requests in seconds", + openaiHistogramBuckets, + ) + + mockMetrics.EXPECT().NewCounter( + "openai_api_total_request_count", + "counts total number of requests made.", + ) + + mockMetrics.EXPECT().NewCounterVec( + "openai_api_token_usage", + "counts number of tokens used.", + ) + + client.InitMetrics() +} + +func Test_AddTrace(t *testing.T) { + config := &Config{ + APIKey: "test-key", + } + + client, _ := NewClient(config) + tracer := trace.NewNoopTracerProvider().Tracer("test-tracer") + client.UseTracer(tracer) + + ctx := context.Background() + resultCtx, span := client.AddTrace(ctx, "test-method") + + assert.NotNil(t, span) + assert.NotEqual(t, ctx, resultCtx) +} + +type mockTransport struct { + response *http.Response + err error +} + +func (m *mockTransport) RoundTrip(*http.Request) (*http.Response, error) { + return m.response, m.err +} + +func Test_Call(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + config := &Config{ + APIKey: "test-key", + BaseURL: "https://api.openai.com", + } + + tests := []struct { + name string + method string + endpoint string + body io.Reader + setupMocks func(*http.Client) + wantErr bool + }{ + { + name: "successful request", + method: http.MethodPost, + endpoint: "/v1/chat/completions", + body: strings.NewReader(`{"test":"data"}`), + setupMocks: func(client *http.Client) { + client.Transport = &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"response":"success"}`)), + }, + } + }, + wantErr: false, + }, + { + name: "failed request", + method: http.MethodPost, + endpoint: "/v1/chat/completions", + body: strings.NewReader(`{"test":"data"}`), + setupMocks: func(client *http.Client) { + client.Transport = &mockTransport{ + err: errNetworkError, + } + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + httpClient := &http.Client{} + if tt.setupMocks != nil { + tt.setupMocks(httpClient) + } + + client, _ := NewClient(config, WithClientHTTP(httpClient)) + client.UseLogger(mockLogger) + client.UseMetrics(mockMetrics) + + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + resp, err := client.Call(context.Background(), tt.method, tt.endpoint, tt.body) + if resp != nil { + defer resp.Body.Close() + } + + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, http.StatusOK, resp.StatusCode) + } + }) + } +} +func Test_Get(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + config := &Config{ + APIKey: "test-key", + BaseURL: "https://api.openai.com", + } + + tests := []struct { + name string + url string + setupMocks func(*http.Client) + want []byte + wantErr bool + }{ + { + name: "successful GET request", + url: "/v1/models", + setupMocks: func(client *http.Client) { + client.Transport = &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"data":"test"}`)), + }, + } + }, + want: []byte(`{"data":"test"}`), + wantErr: false, + }, + { + name: "network error", + url: "/v1/models", + setupMocks: func(client *http.Client) { + client.Transport = &mockTransport{ + err: errNetworkError, + } + }, + want: []byte{}, + wantErr: true, + }, + { + name: "error reading response body", + url: "/v1/models", + setupMocks: func(client *http.Client) { + client.Transport = &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(&errorReader{}), + }, + } + }, + want: []byte{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + httpClient := &http.Client{} + if tt.setupMocks != nil { + tt.setupMocks(httpClient) + } + + client, _ := NewClient(config, WithClientHTTP(httpClient)) + client.UseLogger(mockLogger) + client.UseMetrics(mockMetrics) + + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + got, err := client.Get(context.Background(), tt.url) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +// ErrMockRead is a static error for mock read operations + +// errorReader is a mock reader that always returns an error. +type errorReader struct{} + +func (*errorReader) Read(_ []byte) (n int, err error) { + return 0, errMockRead +} + +func Test_Post(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + config := &Config{ + APIKey: "test-key", + BaseURL: "https://api.openai.com", + } + + tests := []struct { + name string + url string + input interface{} + setupMocks func(*http.Client) + want []byte + wantErr bool + }{ + { + name: "successful POST request", + url: "/v1/completions", + input: map[string]string{ + "prompt": "test", + }, + setupMocks: func(client *http.Client) { + client.Transport = &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result":"success"}`)), + }, + } + }, + want: []byte(`{"result":"success"}`), + wantErr: false, + }, + { + name: "invalid input JSON", + url: "/v1/completions", + input: make(chan int), // Unmarshalable type + setupMocks: func(_ *http.Client) {}, + want: []byte{}, + wantErr: true, + }, + { + name: "network error", + url: "/v1/completions", + input: map[string]string{ + "prompt": "test", + }, + setupMocks: func(client *http.Client) { + client.Transport = &mockTransport{ + err: errNetworkError, + } + }, + want: []byte{}, + wantErr: true, + }, + { + name: "error reading response body", + url: "/v1/completions", + input: map[string]string{ + "prompt": "test", + }, + setupMocks: func(client *http.Client) { + client.Transport = &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(&errorReader{}), + }, + } + }, + want: []byte{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + httpClient := &http.Client{} + if tt.setupMocks != nil { + tt.setupMocks(httpClient) + } + + client, _ := NewClient(config, WithClientHTTP(httpClient)) + client.UseLogger(mockLogger) + client.UseMetrics(mockMetrics) + + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + got, err := client.Post(context.Background(), tt.url, tt.input) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} diff --git a/pkg/gofr/datasource/openai/go.mod b/pkg/gofr/service/openai/go.mod similarity index 100% rename from pkg/gofr/datasource/openai/go.mod rename to pkg/gofr/service/openai/go.mod diff --git a/pkg/gofr/datasource/openai/go.sum b/pkg/gofr/service/openai/go.sum similarity index 100% rename from pkg/gofr/datasource/openai/go.sum rename to pkg/gofr/service/openai/go.sum diff --git a/pkg/gofr/datasource/openai/logger.go b/pkg/gofr/service/openai/logger.go similarity index 100% rename from pkg/gofr/datasource/openai/logger.go rename to pkg/gofr/service/openai/logger.go diff --git a/pkg/gofr/datasource/openai/metrics.go b/pkg/gofr/service/openai/metrics.go similarity index 100% rename from pkg/gofr/datasource/openai/metrics.go rename to pkg/gofr/service/openai/metrics.go diff --git a/pkg/gofr/datasource/openai/mock_logger.go b/pkg/gofr/service/openai/mock_logger.go similarity index 100% rename from pkg/gofr/datasource/openai/mock_logger.go rename to pkg/gofr/service/openai/mock_logger.go diff --git a/pkg/gofr/datasource/openai/mock_metrics.go b/pkg/gofr/service/openai/mock_metrics.go similarity index 100% rename from pkg/gofr/datasource/openai/mock_metrics.go rename to pkg/gofr/service/openai/mock_metrics.go From 630755e0b794730a275bf6d83e9d0b96e658c16c Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Thu, 30 Jan 2025 15:29:00 +0530 Subject: [PATCH 09/17] added code for injecting openai package into container. added a new page in documentation --- pkg/gofr/container/container.go | 2 + pkg/gofr/container/mock_services.go | 143 ++++++++++++++++++++++++++++ pkg/gofr/container/services.go | 28 ++++++ 3 files changed, 173 insertions(+) create mode 100644 pkg/gofr/container/mock_services.go create mode 100644 pkg/gofr/container/services.go diff --git a/pkg/gofr/container/container.go b/pkg/gofr/container/container.go index 1f9d432cc..720cbf7d7 100644 --- a/pkg/gofr/container/container.go +++ b/pkg/gofr/container/container.go @@ -62,6 +62,8 @@ type Container struct { ScyllaDB ScyllaDB SurrealDB SurrealDB + Openai Openai + KVStore KVStore File file.FileSystem diff --git a/pkg/gofr/container/mock_services.go b/pkg/gofr/container/mock_services.go new file mode 100644 index 000000000..283aea779 --- /dev/null +++ b/pkg/gofr/container/mock_services.go @@ -0,0 +1,143 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: services.go +// +// Generated by this command: +// +// mockgen -source=services.go -destination=mock_services.go -package=container +// + +// Package container is a generated GoMock package. +package container + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockOpenai is a mock of Openai interface. +type MockOpenai struct { + ctrl *gomock.Controller + recorder *MockOpenaiMockRecorder + isgomock struct{} +} + +// MockOpenaiMockRecorder is the mock recorder for MockOpenai. +type MockOpenaiMockRecorder struct { + mock *MockOpenai +} + +// NewMockOpenai creates a new mock instance. +func NewMockOpenai(ctrl *gomock.Controller) *MockOpenai { + mock := &MockOpenai{ctrl: ctrl} + mock.recorder = &MockOpenaiMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOpenai) EXPECT() *MockOpenaiMockRecorder { + return m.recorder +} + +// CreateCompletions mocks base method. +func (m *MockOpenai) CreateCompletions(ctx context.Context, r any) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCompletions", ctx, r) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateCompletions indicates an expected call of CreateCompletions. +func (mr *MockOpenaiMockRecorder) CreateCompletions(ctx, r any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCompletions", reflect.TypeOf((*MockOpenai)(nil).CreateCompletions), ctx, r) +} + +// MockOpenaiProvider is a mock of OpenaiProvider interface. +type MockOpenaiProvider struct { + ctrl *gomock.Controller + recorder *MockOpenaiProviderMockRecorder + isgomock struct{} +} + +// MockOpenaiProviderMockRecorder is the mock recorder for MockOpenaiProvider. +type MockOpenaiProviderMockRecorder struct { + mock *MockOpenaiProvider +} + +// NewMockOpenaiProvider creates a new mock instance. +func NewMockOpenaiProvider(ctrl *gomock.Controller) *MockOpenaiProvider { + mock := &MockOpenaiProvider{ctrl: ctrl} + mock.recorder = &MockOpenaiProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOpenaiProvider) EXPECT() *MockOpenaiProviderMockRecorder { + return m.recorder +} + +// CreateCompletions mocks base method. +func (m *MockOpenaiProvider) CreateCompletions(ctx context.Context, r any) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCompletions", ctx, r) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateCompletions indicates an expected call of CreateCompletions. +func (mr *MockOpenaiProviderMockRecorder) CreateCompletions(ctx, r any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCompletions", reflect.TypeOf((*MockOpenaiProvider)(nil).CreateCompletions), ctx, r) +} + +// InitMetrics mocks base method. +func (m *MockOpenaiProvider) InitMetrics() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "InitMetrics") +} + +// InitMetrics indicates an expected call of InitMetrics. +func (mr *MockOpenaiProviderMockRecorder) InitMetrics() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitMetrics", reflect.TypeOf((*MockOpenaiProvider)(nil).InitMetrics)) +} + +// UseLogger mocks base method. +func (m *MockOpenaiProvider) UseLogger(logger any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseLogger", logger) +} + +// UseLogger indicates an expected call of UseLogger. +func (mr *MockOpenaiProviderMockRecorder) UseLogger(logger any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockOpenaiProvider)(nil).UseLogger), logger) +} + +// UseMetrics mocks base method. +func (m *MockOpenaiProvider) UseMetrics(metrics any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseMetrics", metrics) +} + +// UseMetrics indicates an expected call of UseMetrics. +func (mr *MockOpenaiProviderMockRecorder) UseMetrics(metrics any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockOpenaiProvider)(nil).UseMetrics), metrics) +} + +// UseTracer mocks base method. +func (m *MockOpenaiProvider) UseTracer(tracer any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseTracer", tracer) +} + +// UseTracer indicates an expected call of UseTracer. +func (mr *MockOpenaiProviderMockRecorder) UseTracer(tracer any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockOpenaiProvider)(nil).UseTracer), tracer) +} diff --git a/pkg/gofr/container/services.go b/pkg/gofr/container/services.go new file mode 100644 index 000000000..62da0815b --- /dev/null +++ b/pkg/gofr/container/services.go @@ -0,0 +1,28 @@ +package container + +import ( + "context" +) + +// Openai is the interface that wraps the basic endpoint of openai api. + +type Openai interface { + // implementation of chat endpoint of openai api + CreateCompletions(ctx context.Context, r any) (any, error) +} + +type OpenaiProvider interface { + Openai + + // UseLogger set the logger for openai client + UseLogger(logger any) + + // UseMetrics set the logger for openai client + UseMetrics(metrics any) + + // UseTracer set the logger for openai client + UseTracer(tracer any) + + // InitMetrics is used to initializes metrics for the client + InitMetrics() +} From 7929e5bd9228b5e824ded68ea14a342da7dd3fa6 Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Thu, 30 Jan 2025 15:29:58 +0530 Subject: [PATCH 10/17] added documentation and wrote injection code --- docs/advanced-guide/using-openai-api/page.md | 66 ++++++++++++++ docs/navigation.js | 5 ++ pkg/gofr/service/openai/chatcompletion.go | 25 ++++-- .../service/openai/chatcompletion_test.go | 4 +- pkg/gofr/service/openai/go.mod | 6 +- pkg/gofr/service/openai/go.sum | 25 +----- pkg/gofr/service/openai/metrics.go | 8 +- pkg/gofr/service/openai/mock_metrics.go | 90 +++++++++---------- .../service/openai/{client.go => openai.go} | 8 +- .../openai/{client_test.go => openai_test.go} | 2 +- pkg/gofr/services.go | 19 ++++ pkg/gofr/services_test.go | 34 +++++++ 12 files changed, 196 insertions(+), 96 deletions(-) create mode 100644 docs/advanced-guide/using-openai-api/page.md rename pkg/gofr/service/openai/{client.go => openai.go} (95%) rename pkg/gofr/service/openai/{client_test.go => openai_test.go} (99%) create mode 100644 pkg/gofr/services.go create mode 100644 pkg/gofr/services_test.go diff --git a/docs/advanced-guide/using-openai-api/page.md b/docs/advanced-guide/using-openai-api/page.md new file mode 100644 index 000000000..f4f26996a --- /dev/null +++ b/docs/advanced-guide/using-openai-api/page.md @@ -0,0 +1,66 @@ +# Using Openai Api + +GoFr provides an injectable module that integrates OpenAI's API into the GoFr applications. Since it doesn’t come bundled with the framework, this wrapper can be injected seamlessly to extend Gofr's capabilities, enabling developers to utilize OpenAI's powerful AI models effortlessly while maintaining flexibility and scalability. + +GoFr supports any OpenAI api wrapper that implements the following interface. Any other wrapper that implements the interface can be added using `app.AddOpenai()` method, and user's can use openai across applicatoin with `gofr.Context`. + +```go +type Openai interface { + // implementation of chat endpoint of openai api + CreateCompletions(ctx context.Context, r any) (any, error) +} +``` + +### Example +```go +package main + +import ( + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/service/openai" +) + +func main() { + app := gofr.New() + + config := openai.Config{ + APIKey: app.Config.Get("OPENAI_API_KEY"), + Model: "gpt-3.5-turbo", + + // optional config parameters + // BaseURL: "https://api.custom.com", + // Timeout: 10 * time.Second, + // MaxIdleConns: 10, + } + + openaiClient, err := openai.NewClient(&config) + if err != nil { + return + } + + app.AddOpenai(openaiClient) + + app.POST("/chat", Chat) + + app.Run() +} + +func Chat(ctx *gofr.Context) (any, error) { + + var req *openai.CreateCompletionsRequest + + if err := ctx.Bind(&req); err != nil { + return nil, err + } + + println(req) + + resp, err := ctx.Openai.CreateCompletions(ctx, req) + if err != nil { + return nil, err + } + + return resp, nil +} +``` + diff --git a/docs/navigation.js b/docs/navigation.js index e4b2f2433..775deb43a 100644 --- a/docs/navigation.js +++ b/docs/navigation.js @@ -146,6 +146,11 @@ export const navigation = [ title: 'Serving-Static Files', href: '/docs/advanced-guide/serving-static-files', desc: "Know how GoFr automatically serves static content from a static folder in the application directory." + }, + { + title: 'Using Openai Api', + href: '/docs/advanced-guide/using-openai-api', + desc: "Know how to integrate openai api into your applications easily with GoFr's very own openai api wrapper." } ], }, diff --git a/pkg/gofr/service/openai/chatcompletion.go b/pkg/gofr/service/openai/chatcompletion.go index ca94810b4..f5e7c190b 100644 --- a/pkg/gofr/service/openai/chatcompletion.go +++ b/pkg/gofr/service/openai/chatcompletion.go @@ -116,6 +116,7 @@ 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") + errRequestType = errors.New("invalid request type") ) func (e *Error) Error() string { @@ -126,30 +127,38 @@ func (c *Client) CreateCompletionsRaw(ctx context.Context, r *CreateCompletionsR return c.Post(ctx, CompletionsEndpoint, r) } -func (c *Client) CreateCompletions(ctx context.Context, r *CreateCompletionsRequest) (response *CreateCompletionsResponse, err error) { +func (c *Client) CreateCompletions(ctx context.Context, r any) (any, error) { + req, ok := r.(*CreateCompletionsRequest) + if !ok { + c.logger.Errorf("%v", errRequestType) + return nil, errRequestType + } + tracerCtx, span := c.AddTrace(ctx, "CreateCompletions") startTime := time.Now() - if r.Messages == nil && r.Model == "" { + if req.Messages == nil && req.Model == "" { c.logger.Errorf("%v", errMissingBoth) return nil, errMissingBoth } - if r.Messages == nil { + if req.Messages == nil { c.logger.Errorf("%v", errMissingMessages) return nil, errMissingMessages } - if r.Model == "" { + if req.Model == "" { c.logger.Errorf("%v", errMissingModel) return nil, errMissingModel } - raw, err := c.CreateCompletionsRaw(tracerCtx, r) + raw, err := c.CreateCompletionsRaw(tracerCtx, req) if err != nil { - return response, err + return nil, err } + var response CreateCompletionsResponse + err = json.Unmarshal(raw, &response) if err != nil { return nil, err @@ -179,8 +188,8 @@ func (c *Client) SendChatCompletionOperationStats(ctx context.Context, ql *APILo 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) + c.metrics.IncrementCounter(ctx, "openai_api_total_request_count") + c.metrics.DeltaUpDownCounter(ctx, "openai_api_token_usage", float64(ql.Usage.TotalTokens)) if span != nil { defer span.End() diff --git a/pkg/gofr/service/openai/chatcompletion_test.go b/pkg/gofr/service/openai/chatcompletion_test.go index e9ae215ef..ba62cd996 100644 --- a/pkg/gofr/service/openai/chatcompletion_test.go +++ b/pkg/gofr/service/openai/chatcompletion_test.go @@ -47,8 +47,8 @@ func Test_ChatCompletions(t *testing.T) { 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) + metrics.EXPECT().IncrementCounter(gomock.Any(), "openai_api_total_request_count") + metrics.EXPECT().DeltaUpDownCounter(gomock.Any(), "openai_api_token_usage", 10, 20) logger.EXPECT().Debug(gomock.Any()) }, }, diff --git a/pkg/gofr/service/openai/go.mod b/pkg/gofr/service/openai/go.mod index abe266a8b..e6ae47857 100644 --- a/pkg/gofr/service/openai/go.mod +++ b/pkg/gofr/service/openai/go.mod @@ -1,4 +1,4 @@ -module gofr.dev/pkg/gofr/datasource/openai +module gofr.dev/pkg/gofr/service/openai go 1.23.4 @@ -8,7 +8,6 @@ require ( ) require ( - github.com/google/go-querystring v1.1.0 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel/trace v1.33.0 ) @@ -16,8 +15,5 @@ require ( 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/service/openai/go.sum b/pkg/gofr/service/openai/go.sum index 7e0066ae1..dcd447f54 100644 --- a/pkg/gofr/service/openai/go.sum +++ b/pkg/gofr/service/openai/go.sum @@ -1,37 +1,18 @@ 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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/service/openai/metrics.go b/pkg/gofr/service/openai/metrics.go index 36e7eebd4..1aae9a131 100644 --- a/pkg/gofr/service/openai/metrics.go +++ b/pkg/gofr/service/openai/metrics.go @@ -3,11 +3,11 @@ package openai import "context" type Metrics interface { + NewCounter(name, desc string) + NewUpDownCounter(name, desc string) NewHistogram(name, desc string, buckets ...float64) - NewCounter(name, desc string, labels ...string) - NewCounterVec(name, desc string, labels ...string) + IncrementCounter(ctx context.Context, name string, labels ...string) + DeltaUpDownCounter(ctx context.Context, name string, value float64, 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/service/openai/mock_metrics.go b/pkg/gofr/service/openai/mock_metrics.go index b868f541f..9b16d92af 100644 --- a/pkg/gofr/service/openai/mock_metrics.go +++ b/pkg/gofr/service/openai/mock_metrics.go @@ -40,38 +40,50 @@ func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder { return m.recorder } -// NewCounter mocks base method. -func (m *MockMetrics) NewCounter(name, desc string, labels ...string) { +// DeltaUpDownCounter mocks base method. +func (m *MockMetrics) DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string) { m.ctrl.T.Helper() - varargs := []any{name, desc} + varargs := []any{ctx, name, value} for _, a := range labels { varargs = append(varargs, a) } - m.ctrl.Call(m, "NewCounter", varargs...) + m.ctrl.Call(m, "DeltaUpDownCounter", varargs...) } -// NewCounter indicates an expected call of NewCounter. -func (mr *MockMetricsMockRecorder) NewCounter(name, desc any, labels ...any) *gomock.Call { +// DeltaUpDownCounter indicates an expected call of DeltaUpDownCounter. +func (mr *MockMetricsMockRecorder) DeltaUpDownCounter(ctx, name, value 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...) + varargs := append([]any{ctx, name, value}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeltaUpDownCounter", reflect.TypeOf((*MockMetrics)(nil).DeltaUpDownCounter), varargs...) } -// NewCounterVec mocks base method. -func (m *MockMetrics) NewCounterVec(name, desc string, labels ...string) { +// IncrementCounter mocks base method. +func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) { m.ctrl.T.Helper() - varargs := []any{name, desc} + varargs := []any{ctx, name} for _, a := range labels { varargs = append(varargs, a) } - m.ctrl.Call(m, "NewCounterVec", varargs...) + m.ctrl.Call(m, "IncrementCounter", varargs...) } -// NewCounterVec indicates an expected call of NewCounterVec. -func (mr *MockMetricsMockRecorder) NewCounterVec(name, desc any, labels ...any) *gomock.Call { +// IncrementCounter indicates an expected call of IncrementCounter. +func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name 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...) + varargs := append([]any{ctx, name}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...) +} + +// NewCounter mocks base method. +func (m *MockMetrics) NewCounter(name, desc string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "NewCounter", name, desc) +} + +// NewCounter indicates an expected call of NewCounter. +func (mr *MockMetricsMockRecorder) NewCounter(name, desc any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCounter", reflect.TypeOf((*MockMetrics)(nil).NewCounter), name, desc) } // NewHistogram mocks base method. @@ -91,6 +103,18 @@ func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...) } +// NewUpDownCounter mocks base method. +func (m *MockMetrics) NewUpDownCounter(name, desc string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "NewUpDownCounter", name, desc) +} + +// NewUpDownCounter indicates an expected call of NewUpDownCounter. +func (mr *MockMetricsMockRecorder) NewUpDownCounter(name, desc any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewUpDownCounter", reflect.TypeOf((*MockMetrics)(nil).NewUpDownCounter), name, desc) +} + // RecordHistogram mocks base method. func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) { m.ctrl.T.Helper() @@ -107,37 +131,3 @@ func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels 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...) -} diff --git a/pkg/gofr/service/openai/client.go b/pkg/gofr/service/openai/openai.go similarity index 95% rename from pkg/gofr/service/openai/client.go rename to pkg/gofr/service/openai/openai.go index 688926836..7dd7eb56b 100644 --- a/pkg/gofr/service/openai/client.go +++ b/pkg/gofr/service/openai/openai.go @@ -67,7 +67,7 @@ func NewClient(config *Config, opts ...ClientOption) (*Client, error) { Timeout: config.Timeout, Transport: &http.Transport{ MaxIdleConns: config.MaxIdleConns, - IdleConnTimeout: 30 * time.Second, + IdleConnTimeout: 120 * time.Second, }, }, } @@ -79,13 +79,13 @@ func NewClient(config *Config, opts ...ClientOption) (*Client, error) { return c, nil } -func (c *Client) UseLogger(logger interface{}) { +func (c *Client) UseLogger(logger any) { if l, ok := logger.(Logger); ok { c.logger = l } } -func (c *Client) UseMetrics(metrics interface{}) { +func (c *Client) UseMetrics(metrics any) { if m, ok := metrics.(Metrics); ok { c.metrics = m } @@ -111,7 +111,7 @@ func (c *Client) InitMetrics() { "counts total number of requests made.", ) - c.metrics.NewCounterVec( + c.metrics.NewUpDownCounter( "openai_api_token_usage", "counts number of tokens used.", ) diff --git a/pkg/gofr/service/openai/client_test.go b/pkg/gofr/service/openai/openai_test.go similarity index 99% rename from pkg/gofr/service/openai/client_test.go rename to pkg/gofr/service/openai/openai_test.go index c14d2182f..426e093c4 100644 --- a/pkg/gofr/service/openai/client_test.go +++ b/pkg/gofr/service/openai/openai_test.go @@ -152,7 +152,7 @@ func Test_InitMetrics(t *testing.T) { "counts total number of requests made.", ) - mockMetrics.EXPECT().NewCounterVec( + mockMetrics.EXPECT().NewUpDownCounter( "openai_api_token_usage", "counts number of tokens used.", ) diff --git a/pkg/gofr/services.go b/pkg/gofr/services.go new file mode 100644 index 000000000..42e192fb4 --- /dev/null +++ b/pkg/gofr/services.go @@ -0,0 +1,19 @@ +package gofr + +import ( + "go.opentelemetry.io/otel" + "gofr.dev/pkg/gofr/container" +) + +// AddOpenai sets the Openai wrapper in the app's container. +func (a *App) AddOpenai(openai container.OpenaiProvider) { + openai.UseLogger(a.Logger()) + openai.UseMetrics(a.Metrics()) + + tracer := otel.GetTracerProvider().Tracer("gofr-openai") + openai.UseTracer(tracer) + + openai.InitMetrics() + + a.container.Openai = openai +} diff --git a/pkg/gofr/services_test.go b/pkg/gofr/services_test.go new file mode 100644 index 000000000..f8884dcd0 --- /dev/null +++ b/pkg/gofr/services_test.go @@ -0,0 +1,34 @@ +package gofr + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "gofr.dev/pkg/gofr/config" + "gofr.dev/pkg/gofr/container" +) + +func TestApp_AddOpenai(t *testing.T) { + t.Run("Adding OpenAI", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + c := container.NewContainer(config.NewMockConfig(nil)) + + app := &App{ + container: c, + } + + mock := container.NewMockOpenaiProvider(ctrl) + + mock.EXPECT().UseLogger(app.Logger()) + mock.EXPECT().UseMetrics(app.Metrics()) + mock.EXPECT().UseTracer(gomock.Any()) + mock.EXPECT().InitMetrics() + + app.AddOpenai(mock) + + assert.Equal(t, mock, app.container.Openai) + }) +} \ No newline at end of file From c2095628118a7360dedbebd8c52fe6dbfe4fdd1e Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Thu, 30 Jan 2025 19:28:21 +0530 Subject: [PATCH 11/17] resolved changes and fixed mockcontainer_Test that was failing in code quality --- docs/advanced-guide/using-openai-api/page.md | 10 +-- docs/navigation.js | 4 +- pkg/gofr/container/container.go | 2 +- pkg/gofr/container/mock_container.go | 3 + pkg/gofr/container/mock_services.go | 80 +++++++++---------- pkg/gofr/container/mockcontainer_test.go | 15 +--- pkg/gofr/container/services.go | 6 +- pkg/gofr/service/openai/chatcompletion.go | 60 +++++++------- .../service/openai/chatcompletion_test.go | 4 +- pkg/gofr/service/openai/go.mod | 8 +- pkg/gofr/service/openai/go.sum | 13 +++ pkg/gofr/service/openai/logger.go | 18 ++--- pkg/gofr/service/openai/openai_test.go | 2 +- pkg/gofr/services.go | 14 ++-- pkg/gofr/services_test.go | 8 +- 15 files changed, 127 insertions(+), 120 deletions(-) diff --git a/docs/advanced-guide/using-openai-api/page.md b/docs/advanced-guide/using-openai-api/page.md index f4f26996a..2ba65dc3a 100644 --- a/docs/advanced-guide/using-openai-api/page.md +++ b/docs/advanced-guide/using-openai-api/page.md @@ -1,11 +1,11 @@ -# Using Openai Api +# Using OpenAI Api GoFr provides an injectable module that integrates OpenAI's API into the GoFr applications. Since it doesn’t come bundled with the framework, this wrapper can be injected seamlessly to extend Gofr's capabilities, enabling developers to utilize OpenAI's powerful AI models effortlessly while maintaining flexibility and scalability. -GoFr supports any OpenAI api wrapper that implements the following interface. Any other wrapper that implements the interface can be added using `app.AddOpenai()` method, and user's can use openai across applicatoin with `gofr.Context`. +GoFr supports any OpenAI API wrapper that implements the following interface. Any other wrapper that implements the interface can be added using `app.AddOpenAI()` method, and user's can use openai across application with `gofr.Context`. ```go -type Openai interface { +type OpenAI interface { // implementation of chat endpoint of openai api CreateCompletions(ctx context.Context, r any) (any, error) } @@ -33,12 +33,12 @@ func main() { // MaxIdleConns: 10, } - openaiClient, err := openai.NewClient(&config) + openAIClient, err := openai.NewClient(&config) if err != nil { return } - app.AddOpenai(openaiClient) + app.AddOpenAI(openAIClient) app.POST("/chat", Chat) diff --git a/docs/navigation.js b/docs/navigation.js index 1f1c83ccd..7fb5eec29 100644 --- a/docs/navigation.js +++ b/docs/navigation.js @@ -148,9 +148,9 @@ export const navigation = [ desc: "Know how GoFr automatically serves static content from a static folder in the application directory." }, { - title: 'Using Openai Api', + title: 'Using OpenAI Api', href: '/docs/advanced-guide/using-openai-api', - desc: "Know how to integrate openai api into your applications easily with GoFr's very own openai api wrapper." + desc: "Know how to integrate OpenAI api into your applications easily with GoFr's very own OpenAI api wrapper." } ], }, diff --git a/pkg/gofr/container/container.go b/pkg/gofr/container/container.go index 720cbf7d7..a83f36d6a 100644 --- a/pkg/gofr/container/container.go +++ b/pkg/gofr/container/container.go @@ -62,7 +62,7 @@ type Container struct { ScyllaDB ScyllaDB SurrealDB SurrealDB - Openai Openai + OpenAI OpenAI KVStore KVStore diff --git a/pkg/gofr/container/mock_container.go b/pkg/gofr/container/mock_container.go index 8d36eb8c4..97f9d67c0 100644 --- a/pkg/gofr/container/mock_container.go +++ b/pkg/gofr/container/mock_container.go @@ -87,6 +87,9 @@ func NewMockContainer(t *testing.T, options ...options) (*Container, *Mocks) { opentsdbMock := NewMockOpenTSDBProvider(ctrl) container.OpenTSDB = opentsdbMock + opnAIMock := NewMockOpenAIProvider(ctrl) + container.OpenAI = opnAIMock + var httpMock *service.MockHTTP container.Services = make(map[string]service.HTTP) diff --git a/pkg/gofr/container/mock_services.go b/pkg/gofr/container/mock_services.go index 283aea779..d0bc007b1 100644 --- a/pkg/gofr/container/mock_services.go +++ b/pkg/gofr/container/mock_services.go @@ -16,32 +16,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockOpenai is a mock of Openai interface. -type MockOpenai struct { +// MockOpenAI is a mock of OpenAI interface. +type MockOpenAI struct { ctrl *gomock.Controller - recorder *MockOpenaiMockRecorder + recorder *MockOpenAIMockRecorder isgomock struct{} } -// MockOpenaiMockRecorder is the mock recorder for MockOpenai. -type MockOpenaiMockRecorder struct { - mock *MockOpenai +// MockOpenAIMockRecorder is the mock recorder for MockOpenAI. +type MockOpenAIMockRecorder struct { + mock *MockOpenAI } -// NewMockOpenai creates a new mock instance. -func NewMockOpenai(ctrl *gomock.Controller) *MockOpenai { - mock := &MockOpenai{ctrl: ctrl} - mock.recorder = &MockOpenaiMockRecorder{mock} +// NewMockOpenAI creates a new mock instance. +func NewMockOpenAI(ctrl *gomock.Controller) *MockOpenAI { + mock := &MockOpenAI{ctrl: ctrl} + mock.recorder = &MockOpenAIMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockOpenai) EXPECT() *MockOpenaiMockRecorder { +func (m *MockOpenAI) EXPECT() *MockOpenAIMockRecorder { return m.recorder } // CreateCompletions mocks base method. -func (m *MockOpenai) CreateCompletions(ctx context.Context, r any) (any, error) { +func (m *MockOpenAI) CreateCompletions(ctx context.Context, r any) (any, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateCompletions", ctx, r) ret0, _ := ret[0].(any) @@ -50,37 +50,37 @@ func (m *MockOpenai) CreateCompletions(ctx context.Context, r any) (any, error) } // CreateCompletions indicates an expected call of CreateCompletions. -func (mr *MockOpenaiMockRecorder) CreateCompletions(ctx, r any) *gomock.Call { +func (mr *MockOpenAIMockRecorder) CreateCompletions(ctx, r any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCompletions", reflect.TypeOf((*MockOpenai)(nil).CreateCompletions), ctx, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCompletions", reflect.TypeOf((*MockOpenAI)(nil).CreateCompletions), ctx, r) } -// MockOpenaiProvider is a mock of OpenaiProvider interface. -type MockOpenaiProvider struct { +// MockOpenAIProvider is a mock of OpenAIProvider interface. +type MockOpenAIProvider struct { ctrl *gomock.Controller - recorder *MockOpenaiProviderMockRecorder + recorder *MockOpenAIProviderMockRecorder isgomock struct{} } -// MockOpenaiProviderMockRecorder is the mock recorder for MockOpenaiProvider. -type MockOpenaiProviderMockRecorder struct { - mock *MockOpenaiProvider +// MockOpenAIProviderMockRecorder is the mock recorder for MockOpenAIProvider. +type MockOpenAIProviderMockRecorder struct { + mock *MockOpenAIProvider } -// NewMockOpenaiProvider creates a new mock instance. -func NewMockOpenaiProvider(ctrl *gomock.Controller) *MockOpenaiProvider { - mock := &MockOpenaiProvider{ctrl: ctrl} - mock.recorder = &MockOpenaiProviderMockRecorder{mock} +// NewMockOpenAIProvider creates a new mock instance. +func NewMockOpenAIProvider(ctrl *gomock.Controller) *MockOpenAIProvider { + mock := &MockOpenAIProvider{ctrl: ctrl} + mock.recorder = &MockOpenAIProviderMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockOpenaiProvider) EXPECT() *MockOpenaiProviderMockRecorder { +func (m *MockOpenAIProvider) EXPECT() *MockOpenAIProviderMockRecorder { return m.recorder } // CreateCompletions mocks base method. -func (m *MockOpenaiProvider) CreateCompletions(ctx context.Context, r any) (any, error) { +func (m *MockOpenAIProvider) CreateCompletions(ctx context.Context, r any) (any, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateCompletions", ctx, r) ret0, _ := ret[0].(any) @@ -89,55 +89,55 @@ func (m *MockOpenaiProvider) CreateCompletions(ctx context.Context, r any) (any, } // CreateCompletions indicates an expected call of CreateCompletions. -func (mr *MockOpenaiProviderMockRecorder) CreateCompletions(ctx, r any) *gomock.Call { +func (mr *MockOpenAIProviderMockRecorder) CreateCompletions(ctx, r any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCompletions", reflect.TypeOf((*MockOpenaiProvider)(nil).CreateCompletions), ctx, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCompletions", reflect.TypeOf((*MockOpenAIProvider)(nil).CreateCompletions), ctx, r) } // InitMetrics mocks base method. -func (m *MockOpenaiProvider) InitMetrics() { +func (m *MockOpenAIProvider) InitMetrics() { m.ctrl.T.Helper() m.ctrl.Call(m, "InitMetrics") } // InitMetrics indicates an expected call of InitMetrics. -func (mr *MockOpenaiProviderMockRecorder) InitMetrics() *gomock.Call { +func (mr *MockOpenAIProviderMockRecorder) InitMetrics() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitMetrics", reflect.TypeOf((*MockOpenaiProvider)(nil).InitMetrics)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitMetrics", reflect.TypeOf((*MockOpenAIProvider)(nil).InitMetrics)) } // UseLogger mocks base method. -func (m *MockOpenaiProvider) UseLogger(logger any) { +func (m *MockOpenAIProvider) UseLogger(logger any) { m.ctrl.T.Helper() m.ctrl.Call(m, "UseLogger", logger) } // UseLogger indicates an expected call of UseLogger. -func (mr *MockOpenaiProviderMockRecorder) UseLogger(logger any) *gomock.Call { +func (mr *MockOpenAIProviderMockRecorder) UseLogger(logger any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockOpenaiProvider)(nil).UseLogger), logger) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockOpenAIProvider)(nil).UseLogger), logger) } // UseMetrics mocks base method. -func (m *MockOpenaiProvider) UseMetrics(metrics any) { +func (m *MockOpenAIProvider) UseMetrics(metrics any) { m.ctrl.T.Helper() m.ctrl.Call(m, "UseMetrics", metrics) } // UseMetrics indicates an expected call of UseMetrics. -func (mr *MockOpenaiProviderMockRecorder) UseMetrics(metrics any) *gomock.Call { +func (mr *MockOpenAIProviderMockRecorder) UseMetrics(metrics any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockOpenaiProvider)(nil).UseMetrics), metrics) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockOpenAIProvider)(nil).UseMetrics), metrics) } // UseTracer mocks base method. -func (m *MockOpenaiProvider) UseTracer(tracer any) { +func (m *MockOpenAIProvider) UseTracer(tracer any) { m.ctrl.T.Helper() m.ctrl.Call(m, "UseTracer", tracer) } // UseTracer indicates an expected call of UseTracer. -func (mr *MockOpenaiProviderMockRecorder) UseTracer(tracer any) *gomock.Call { +func (mr *MockOpenAIProviderMockRecorder) UseTracer(tracer any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockOpenaiProvider)(nil).UseTracer), tracer) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockOpenAIProvider)(nil).UseTracer), tracer) } diff --git a/pkg/gofr/container/mockcontainer_test.go b/pkg/gofr/container/mockcontainer_test.go index 38264cf1f..e483a6da3 100644 --- a/pkg/gofr/container/mockcontainer_test.go +++ b/pkg/gofr/container/mockcontainer_test.go @@ -15,26 +15,13 @@ import ( ) func Test_HttpServiceMock(t *testing.T) { - test := struct { - desc string - path string - statusCode int - expectedRes string - }{ - - desc: "simple service handler", - path: "/fact", - expectedRes: `{"data":{"fact":"Cats have 3 eyelids.","length":20}}` + "\n", - statusCode: 200, - } - httpservices := []string{"cat-facts", "cat-facts1", "cat-facts2"} _, mock := NewMockContainer(t, WithMockHTTPService(httpservices...)) res := httptest.NewRecorder() res.Body = bytes.NewBufferString(`{"fact":"Cats have 3 eyelids.","length":20}` + "\n") - res.Code = test.statusCode + res.Code = 200 result := res.Result() // Setting mock expectations diff --git a/pkg/gofr/container/services.go b/pkg/gofr/container/services.go index 62da0815b..de242ebf7 100644 --- a/pkg/gofr/container/services.go +++ b/pkg/gofr/container/services.go @@ -6,13 +6,13 @@ import ( // Openai is the interface that wraps the basic endpoint of openai api. -type Openai interface { +type OpenAI interface { // implementation of chat endpoint of openai api CreateCompletions(ctx context.Context, r any) (any, error) } -type OpenaiProvider interface { - Openai +type OpenAIProvider interface { + OpenAI // UseLogger set the logger for openai client UseLogger(logger any) diff --git a/pkg/gofr/service/openai/chatcompletion.go b/pkg/gofr/service/openai/chatcompletion.go index f5e7c190b..1487c22de 100644 --- a/pkg/gofr/service/openai/chatcompletion.go +++ b/pkg/gofr/service/openai/chatcompletion.go @@ -18,7 +18,7 @@ type CreateCompletionsRequest struct { Model string `json:"model,omitempty"` Store bool `json:"store,omitempty"` ReasoningEffort string `json:"reasoning_effort,omitempty"` - MetaData interface{} `json:"metadata,omitempty"` // object or null + MetaData any `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"` @@ -27,7 +27,7 @@ type CreateCompletionsRequest struct { MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` N int `json:"n,omitempty"` Modalities []string `json:"modalities,omitempty"` - Prediction interface{} `json:"prediction,omitempty"` + Prediction any `json:"prediction,omitempty"` PresencePenalty float64 `json:"presence_penalty,omitempty"` Audio struct { @@ -35,11 +35,11 @@ type CreateCompletionsRequest struct { 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"` + ResponseFormat any `json:"response_format,omitempty"` + Seed int `json:"seed,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` + Stop any `json:"stop,omitempty"` + Stream bool `json:"stream,omitempty"` StreamOptions struct { IncludeUsage bool `json:"include_usage,omitempty"` @@ -51,17 +51,17 @@ type CreateCompletionsRequest struct { 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"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Parameters any `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"` + ToolChoice any `json:"tool_choice,omitempty"` + ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"` + Suffix string `json:"suffix,omitempty"` + User string `json:"user,omitempty"` } type Message struct { @@ -82,14 +82,14 @@ type CreateCompletionsResponse 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"` + Role string `json:"role,omitempty"` + Content string `json:"content,omitempty"` + Refusal string `json:"refusal,omitempty"` + ToolCalls any `json:"tool_calls,omitempty"` } `json:"message"` - Logprobs interface{} `json:"logprobs,omitempty"` - FinishReason string `json:"finish_reason,omitempty"` + Logprobs any `json:"logprobs,omitempty"` + FinishReason string `json:"finish_reason,omitempty"` } `json:"choices,omitempty"` Usage Usage `json:"usage,omitempty"` @@ -98,18 +98,18 @@ type CreateCompletionsResponse struct { } 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"` + PromptTokens int `json:"prompt_tokens,omitempty"` + CompletionTokens int `json:"completion_tokens,omitempty"` + TotalTokens int `json:"total_tokens,omitempty"` + CompletionTokensDetails any `json:"completion_tokens_details,omitempty"` + PromptTokensDetails any `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"` + Message string `json:"message,omitempty"` + Type string `json:"type,omitempty"` + Param any `json:"param,omitempty"` + Code any `json:"code,omitempty"` } var ( diff --git a/pkg/gofr/service/openai/chatcompletion_test.go b/pkg/gofr/service/openai/chatcompletion_test.go index ba62cd996..7fcbd61eb 100644 --- a/pkg/gofr/service/openai/chatcompletion_test.go +++ b/pkg/gofr/service/openai/chatcompletion_test.go @@ -48,7 +48,7 @@ func Test_ChatCompletions(t *testing.T) { setupMocks: func(logger *MockLogger, metrics *MockMetrics) { metrics.EXPECT().RecordHistogram(gomock.Any(), "openai_api_request_duration", gomock.Any()) metrics.EXPECT().IncrementCounter(gomock.Any(), "openai_api_total_request_count") - metrics.EXPECT().DeltaUpDownCounter(gomock.Any(), "openai_api_token_usage", 10, 20) + metrics.EXPECT().DeltaUpDownCounter(gomock.Any(), "openai_api_token_usage", 30.0) logger.EXPECT().Debug(gomock.Any()) }, }, @@ -118,7 +118,7 @@ func Test_ChatCompletions(t *testing.T) { } } -func setupTestServer(t *testing.T, path string, response interface{}) *httptest.Server { +func setupTestServer(t *testing.T, path string, response any) *httptest.Server { t.Helper() server := httptest.NewServer( diff --git a/pkg/gofr/service/openai/go.mod b/pkg/gofr/service/openai/go.mod index e6ae47857..13650e2ac 100644 --- a/pkg/gofr/service/openai/go.mod +++ b/pkg/gofr/service/openai/go.mod @@ -3,17 +3,21 @@ module gofr.dev/pkg/gofr/service/openai go 1.23.4 require ( - go.opentelemetry.io/otel v1.33.0 + go.opentelemetry.io/otel v1.34.0 go.uber.org/mock v0.5.0 ) require ( github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/otel/trace v1.33.0 + go.opentelemetry.io/otel/trace v1.34.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/gofr/service/openai/go.sum b/pkg/gofr/service/openai/go.sum index dcd447f54..d3b9b2bd3 100644 --- a/pkg/gofr/service/openai/go.sum +++ b/pkg/gofr/service/openai/go.sum @@ -1,15 +1,28 @@ 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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 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 v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 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.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/pkg/gofr/service/openai/logger.go b/pkg/gofr/service/openai/logger.go index 8ea428b14..08cd29073 100644 --- a/pkg/gofr/service/openai/logger.go +++ b/pkg/gofr/service/openai/logger.go @@ -1,10 +1,10 @@ package openai type Logger interface { - Debug(args ...interface{}) - Debugf(pattern string, args ...interface{}) - Logf(pattern string, args ...interface{}) - Errorf(pattern string, args ...interface{}) + Debug(args ...any) + Debugf(pattern string, args ...any) + Logf(pattern string, args ...any) + Errorf(pattern string, args ...any) } type APILog struct { @@ -17,11 +17,11 @@ type APILog struct { 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"` + PromptTokens int `json:"prompt_tokens,omitempty"` + CompletionTokens int `json:"completion_tokens,omitempty"` + TotalTokens int `json:"total_tokens,omitempty"` + CompletionTokensDetails any `json:"completion_tokens_details,omitempty"` + PromptTokensDetails any `json:"prompt_tokens_details,omitempty"` } `json:"usage,omitempty"` Error *Error `json:"error,omitempty"` diff --git a/pkg/gofr/service/openai/openai_test.go b/pkg/gofr/service/openai/openai_test.go index 426e093c4..a384f2f46 100644 --- a/pkg/gofr/service/openai/openai_test.go +++ b/pkg/gofr/service/openai/openai_test.go @@ -371,7 +371,7 @@ func Test_Post(t *testing.T) { tests := []struct { name string url string - input interface{} + input any setupMocks func(*http.Client) want []byte wantErr bool diff --git a/pkg/gofr/services.go b/pkg/gofr/services.go index 42e192fb4..0369e3062 100644 --- a/pkg/gofr/services.go +++ b/pkg/gofr/services.go @@ -6,14 +6,14 @@ import ( ) // AddOpenai sets the Openai wrapper in the app's container. -func (a *App) AddOpenai(openai container.OpenaiProvider) { - openai.UseLogger(a.Logger()) - openai.UseMetrics(a.Metrics()) +func (a *App) AddOpenAI(openAI container.OpenAIProvider) { + openAI.UseLogger(a.Logger()) + openAI.UseMetrics(a.Metrics()) - tracer := otel.GetTracerProvider().Tracer("gofr-openai") - openai.UseTracer(tracer) + tracer := otel.GetTracerProvider().Tracer("gofr-openAI") + openAI.UseTracer(tracer) - openai.InitMetrics() + openAI.InitMetrics() - a.container.Openai = openai + a.container.OpenAI = openAI } diff --git a/pkg/gofr/services_test.go b/pkg/gofr/services_test.go index f8884dcd0..b58fe5925 100644 --- a/pkg/gofr/services_test.go +++ b/pkg/gofr/services_test.go @@ -9,7 +9,7 @@ import ( "gofr.dev/pkg/gofr/container" ) -func TestApp_AddOpenai(t *testing.T) { +func TestApp_AddOpenAI(t *testing.T) { t.Run("Adding OpenAI", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -20,15 +20,15 @@ func TestApp_AddOpenai(t *testing.T) { container: c, } - mock := container.NewMockOpenaiProvider(ctrl) + mock := container.NewMockOpenAIProvider(ctrl) mock.EXPECT().UseLogger(app.Logger()) mock.EXPECT().UseMetrics(app.Metrics()) mock.EXPECT().UseTracer(gomock.Any()) mock.EXPECT().InitMetrics() - app.AddOpenai(mock) + app.AddOpenAI(mock) - assert.Equal(t, mock, app.container.Openai) + assert.Equal(t, mock, app.container.OpenAI) }) } \ No newline at end of file From 96f2da6f6573f92a3e457f1b36081dce9ed71dee Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Fri, 31 Jan 2025 12:56:36 +0530 Subject: [PATCH 12/17] solved linting error in services.go file --- pkg/gofr/services.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/services.go b/pkg/gofr/services.go index 0369e3062..592963282 100644 --- a/pkg/gofr/services.go +++ b/pkg/gofr/services.go @@ -5,7 +5,7 @@ import ( "gofr.dev/pkg/gofr/container" ) -// AddOpenai sets the Openai wrapper in the app's container. +// AddOpenAI sets the OpenAI wrapper in the app's container. func (a *App) AddOpenAI(openAI container.OpenAIProvider) { openAI.UseLogger(a.Logger()) openAI.UseMetrics(a.Metrics()) From 30cae8f077aeda22c75e7d02ea484e21c8e24776 Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Fri, 31 Jan 2025 13:01:44 +0530 Subject: [PATCH 13/17] more linter changes --- pkg/gofr/container/mock_container.go | 4 ++-- pkg/gofr/container/services.go | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/gofr/container/mock_container.go b/pkg/gofr/container/mock_container.go index 97f9d67c0..4a9daf0c0 100644 --- a/pkg/gofr/container/mock_container.go +++ b/pkg/gofr/container/mock_container.go @@ -87,8 +87,8 @@ func NewMockContainer(t *testing.T, options ...options) (*Container, *Mocks) { opentsdbMock := NewMockOpenTSDBProvider(ctrl) container.OpenTSDB = opentsdbMock - opnAIMock := NewMockOpenAIProvider(ctrl) - container.OpenAI = opnAIMock + openAIMock := NewMockOpenAIProvider(ctrl) + container.OpenAI = openAIMock var httpMock *service.MockHTTP diff --git a/pkg/gofr/container/services.go b/pkg/gofr/container/services.go index de242ebf7..0e326462a 100644 --- a/pkg/gofr/container/services.go +++ b/pkg/gofr/container/services.go @@ -4,23 +4,23 @@ import ( "context" ) -// Openai is the interface that wraps the basic endpoint of openai api. +// OpenAI is the interface that wraps the basic endpoint of OpenAI API. type OpenAI interface { - // implementation of chat endpoint of openai api + // implementation of chat endpoint of OpenAI API CreateCompletions(ctx context.Context, r any) (any, error) } type OpenAIProvider interface { OpenAI - // UseLogger set the logger for openai client + // UseLogger set the logger for OpenAI client UseLogger(logger any) - // UseMetrics set the logger for openai client + // UseMetrics set the logger for OpenAI client UseMetrics(metrics any) - // UseTracer set the logger for openai client + // UseTracer set the logger for OpenAI client UseTracer(tracer any) // InitMetrics is used to initializes metrics for the client From 658a91ad90a8bb15db113ac1129feed9b7f3d7be Mon Sep 17 00:00:00 2001 From: umang01-hash Date: Fri, 31 Jan 2025 14:41:37 +0530 Subject: [PATCH 14/17] resolve linters --- pkg/gofr/services.go | 1 + pkg/gofr/services_test.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/gofr/services.go b/pkg/gofr/services.go index 592963282..8101326be 100644 --- a/pkg/gofr/services.go +++ b/pkg/gofr/services.go @@ -2,6 +2,7 @@ package gofr import ( "go.opentelemetry.io/otel" + "gofr.dev/pkg/gofr/container" ) diff --git a/pkg/gofr/services_test.go b/pkg/gofr/services_test.go index b58fe5925..b2f98a9b1 100644 --- a/pkg/gofr/services_test.go +++ b/pkg/gofr/services_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "gofr.dev/pkg/gofr/config" "gofr.dev/pkg/gofr/container" ) @@ -31,4 +32,4 @@ func TestApp_AddOpenAI(t *testing.T) { assert.Equal(t, mock, app.container.OpenAI) }) -} \ No newline at end of file +} From 59c03eb270d1a9ccafb3e0568dc210f386bed0c6 Mon Sep 17 00:00:00 2001 From: umang01-hash Date: Fri, 31 Jan 2025 14:53:12 +0530 Subject: [PATCH 15/17] remove deprecated methods in test --- pkg/gofr/service/openai/openai_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/gofr/service/openai/openai_test.go b/pkg/gofr/service/openai/openai_test.go index a384f2f46..e1f16c71f 100644 --- a/pkg/gofr/service/openai/openai_test.go +++ b/pkg/gofr/service/openai/openai_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel" "go.uber.org/mock/gomock" ) @@ -114,7 +114,7 @@ func Test_UseTracer(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - tracer := trace.NewNoopTracerProvider().Tracer("test-tracer") + tracer := otel.GetTracerProvider().Tracer("gofr-openAI") config := &Config{ APIKey: "key", @@ -166,7 +166,7 @@ func Test_AddTrace(t *testing.T) { } client, _ := NewClient(config) - tracer := trace.NewNoopTracerProvider().Tracer("test-tracer") + tracer := otel.GetTracerProvider().Tracer("gofr-openAI") client.UseTracer(tracer) ctx := context.Background() From c20bc1ed5efba3588e3afa197a2cc43c54ee233e Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Fri, 31 Jan 2025 23:20:22 +0530 Subject: [PATCH 16/17] resolved changes --- pkg/gofr/container/services.go | 1 - pkg/gofr/service/openai/openai.go | 2 +- pkg/gofr/services.go | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/gofr/container/services.go b/pkg/gofr/container/services.go index 0e326462a..61f8ecd83 100644 --- a/pkg/gofr/container/services.go +++ b/pkg/gofr/container/services.go @@ -5,7 +5,6 @@ import ( ) // OpenAI is the interface that wraps the basic endpoint of OpenAI API. - type OpenAI interface { // implementation of chat endpoint of OpenAI API CreateCompletions(ctx context.Context, r any) (any, error) diff --git a/pkg/gofr/service/openai/openai.go b/pkg/gofr/service/openai/openai.go index 7dd7eb56b..587f4709f 100644 --- a/pkg/gofr/service/openai/openai.go +++ b/pkg/gofr/service/openai/openai.go @@ -30,7 +30,7 @@ type Client struct { } var ( - errorMissingAPIKey = errors.New("api key not provided") + errorMissingAPIKey = errors.New("API key not provided") ) type ClientOption func(*Client) diff --git a/pkg/gofr/services.go b/pkg/gofr/services.go index 8101326be..061233d06 100644 --- a/pkg/gofr/services.go +++ b/pkg/gofr/services.go @@ -11,7 +11,7 @@ func (a *App) AddOpenAI(openAI container.OpenAIProvider) { openAI.UseLogger(a.Logger()) openAI.UseMetrics(a.Metrics()) - tracer := otel.GetTracerProvider().Tracer("gofr-openAI") + tracer := otel.GetTracerProvider().Tracer("gofr-openai") openAI.UseTracer(tracer) openAI.InitMetrics() From 88e7cb4a52d1dd8cfeead33e67ed7f68d4c48332 Mon Sep 17 00:00:00 2001 From: yashsojitra97 Date: Thu, 6 Feb 2025 18:13:32 +0530 Subject: [PATCH 17/17] resolved changes --- docs/advanced-guide/using-openai-api/page.md | 2 -- pkg/gofr/service/openai/chatcompletion.go | 2 +- pkg/gofr/service/openai/openai.go | 10 +++++----- pkg/gofr/service/openai/openai_test.go | 6 +++--- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/docs/advanced-guide/using-openai-api/page.md b/docs/advanced-guide/using-openai-api/page.md index 2ba65dc3a..6d52e6a19 100644 --- a/docs/advanced-guide/using-openai-api/page.md +++ b/docs/advanced-guide/using-openai-api/page.md @@ -53,8 +53,6 @@ func Chat(ctx *gofr.Context) (any, error) { return nil, err } - println(req) - resp, err := ctx.Openai.CreateCompletions(ctx, req) if err != nil { return nil, err diff --git a/pkg/gofr/service/openai/chatcompletion.go b/pkg/gofr/service/openai/chatcompletion.go index 1487c22de..fd98ed32d 100644 --- a/pkg/gofr/service/openai/chatcompletion.go +++ b/pkg/gofr/service/openai/chatcompletion.go @@ -124,7 +124,7 @@ func (e *Error) Error() string { } func (c *Client) CreateCompletionsRaw(ctx context.Context, r *CreateCompletionsRequest) ([]byte, error) { - return c.Post(ctx, CompletionsEndpoint, r) + return c.post(ctx, CompletionsEndpoint, r) } func (c *Client) CreateCompletions(ctx context.Context, r any) (any, error) { diff --git a/pkg/gofr/service/openai/openai.go b/pkg/gofr/service/openai/openai.go index 587f4709f..7a404f5a9 100644 --- a/pkg/gofr/service/openai/openai.go +++ b/pkg/gofr/service/openai/openai.go @@ -127,7 +127,7 @@ func (c *Client) AddTrace(ctx context.Context, method string) (context.Context, return ctx, nil } -func (c *Client) Post(ctx context.Context, url string, input any) (response []byte, err error) { +func (c *Client) post(ctx context.Context, url string, input any) (response []byte, err error) { response = make([]byte, 0) reqJSON, err := json.Marshal(input) @@ -136,7 +136,7 @@ func (c *Client) Post(ctx context.Context, url string, input any) (response []by return response, err } - resp, err := c.Call(ctx, http.MethodPost, url, bytes.NewReader(reqJSON)) + resp, err := c.call(ctx, http.MethodPost, url, bytes.NewReader(reqJSON)) if err != nil { c.logger.Errorf("%v", err) return response, err @@ -152,8 +152,8 @@ func (c *Client) Post(ctx context.Context, url string, input any) (response []by } // 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) +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 @@ -169,7 +169,7 @@ func (c *Client) Get(ctx context.Context, url string) (response []byte, err erro } // Call makes a request. -func (c *Client) Call(ctx context.Context, method, endpoint string, body io.Reader) (response *http.Response, err error) { +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) diff --git a/pkg/gofr/service/openai/openai_test.go b/pkg/gofr/service/openai/openai_test.go index e1f16c71f..ebc1df1be 100644 --- a/pkg/gofr/service/openai/openai_test.go +++ b/pkg/gofr/service/openai/openai_test.go @@ -247,7 +247,7 @@ func Test_Call(t *testing.T) { mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - resp, err := client.Call(context.Background(), tt.method, tt.endpoint, tt.body) + resp, err := client.call(context.Background(), tt.method, tt.endpoint, tt.body) if resp != nil { defer resp.Body.Close() } @@ -335,7 +335,7 @@ func Test_Get(t *testing.T) { mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - got, err := client.Get(context.Background(), tt.url) + got, err := client.get(context.Background(), tt.url) if tt.wantErr { require.Error(t, err) @@ -447,7 +447,7 @@ func Test_Post(t *testing.T) { mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - got, err := client.Post(context.Background(), tt.url, tt.input) + got, err := client.post(context.Background(), tt.url, tt.input) if tt.wantErr { require.Error(t, err)