diff --git a/pkg/api/http/handlers.go b/pkg/api/http/handlers.go index 581b270e..ab1fc15b 100644 --- a/pkg/api/http/handlers.go +++ b/pkg/api/http/handlers.go @@ -2,7 +2,6 @@ package http import ( "context" - "errors" "sync" "github.com/EinStack/glide/pkg/telemetry" @@ -38,9 +37,7 @@ type Handler = func(c *fiber.Ctx) error func LangChatHandler(routerManager *routers.RouterManager) Handler { return func(c *fiber.Ctx) error { if !c.Is("json") { - return c.Status(fiber.StatusBadRequest).JSON(ErrorSchema{ - Message: "Glide accepts only JSON payloads", - }) + return c.Status(fiber.StatusBadRequest).JSON(schemas.ErrUnsupportedMediaType) } // Unmarshal request body @@ -48,29 +45,25 @@ func LangChatHandler(routerManager *routers.RouterManager) Handler { err := c.BodyParser(&req) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(ErrorSchema{ - Message: err.Error(), - }) + return c.Status(fiber.StatusBadRequest).JSON(schemas.NewPayloadParseErr(err)) } // Get router ID from path routerID := c.Params("router") + router, err := routerManager.GetLangRouter(routerID) + if err != nil { + httpErr := schemas.FromErr(err) - if errors.Is(err, routers.ErrRouterNotFound) { - // Return not found error - return c.Status(fiber.StatusNotFound).JSON(ErrorSchema{ - Message: err.Error(), - }) + return c.Status(httpErr.Status).JSON(httpErr) } // Chat with router resp, err := router.Chat(c.Context(), req) if err != nil { - // Return internal server error - return c.Status(fiber.StatusInternalServerError).JSON(ErrorSchema{ - Message: err.Error(), - }) + httpErr := schemas.FromErr(err) + + return c.Status(httpErr.Status).JSON(httpErr) } // Return chat response @@ -85,9 +78,9 @@ func LangStreamRouterValidator(routerManager *routers.RouterManager) Handler { _, err := routerManager.GetLangRouter(routerID) if err != nil { - return c.Status(fiber.StatusNotFound).JSON(ErrorSchema{ - Message: err.Error(), - }) + httpErr := schemas.FromErr(err) + + return c.Status(httpErr.Status).JSON(httpErr) } return c.Next() @@ -208,7 +201,5 @@ func HealthHandler(c *fiber.Ctx) error { } func NotFoundHandler(c *fiber.Ctx) error { - return c.Status(fiber.StatusNotFound).JSON(ErrorSchema{ - Message: "The route is not found", - }) + return c.Status(fiber.StatusNotFound).JSON(schemas.ErrRouteNotFound) } diff --git a/pkg/api/http/schemas.go b/pkg/api/http/schemas.go index 3ee515eb..f3fbb701 100644 --- a/pkg/api/http/schemas.go +++ b/pkg/api/http/schemas.go @@ -2,10 +2,6 @@ package http import "github.com/EinStack/glide/pkg/routers" -type ErrorSchema struct { - Message string `json:"message"` -} - type HealthSchema struct { Healthy bool `json:"healthy"` } diff --git a/pkg/api/schemas/chat_stream.go b/pkg/api/schemas/chat_stream.go index 983d2242..f05644e3 100644 --- a/pkg/api/schemas/chat_stream.go +++ b/pkg/api/schemas/chat_stream.go @@ -6,22 +6,14 @@ type ( Metadata = map[string]any EventType = string FinishReason = string - ErrorCode = string ) var ( - Complete FinishReason = "complete" - MaxTokens FinishReason = "max_tokens" - ContentFiltered FinishReason = "content_filtered" - ErrorReason FinishReason = "error" - OtherReason FinishReason = "other" -) - -var ( - NoModelConfigured ErrorCode = "no_model_configured" - ModelUnavailable ErrorCode = "model_unavailable" - AllModelsUnavailable ErrorCode = "all_models_unavailable" - UnknownError ErrorCode = "unknown_error" + ReasonComplete FinishReason = "complete" + ReasonMaxTokens FinishReason = "max_tokens" + ReasonContentFiltered FinishReason = "content_filtered" + ReasonError FinishReason = "error" + ReasonOther FinishReason = "other" ) type StreamRequestID = string @@ -70,7 +62,7 @@ type ChatStreamChunk struct { } type ChatStreamError struct { - ErrCode ErrorCode `json:"errCode"` + ErrCode ErrorName `json:"errCode"` Message string `json:"message"` FinishReason *FinishReason `json:"finishReason,omitempty"` } @@ -93,7 +85,7 @@ func NewChatStreamChunk( func NewChatStreamError( reqID StreamRequestID, routerID string, - errCode ErrorCode, + errCode ErrorName, errMsg string, reqMetadata *Metadata, finishReason *FinishReason, diff --git a/pkg/api/schemas/errors.go b/pkg/api/schemas/errors.go new file mode 100644 index 00000000..3a27e7df --- /dev/null +++ b/pkg/api/schemas/errors.go @@ -0,0 +1,82 @@ +package schemas + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" +) + +type ErrorName = string + +var ( + // General API errors + UnsupportedMediaType ErrorName = "http.unsupported_media_type" + RouteNotFound ErrorName = "http.not_found" + PayloadParseError ErrorName = "http.payload_parse_error" + UnknownError ErrorName = "http.unknown_error" + + // Router-specific errors + RouterNotFound ErrorName = "routers.not_found" + NoModelConfigured ErrorName = "routers.no_model_configured" + ModelUnavailable ErrorName = "routers.model_unavailable" + AllModelsUnavailable ErrorName = "routers.all_models_unavailable" +) + +// Error / Error contains more context than the built-in error type, +// so we know information like error code and message that are useful to propagate to clients +type Error struct { + Status int `json:"-"` + Name string `json:"name"` + Message string `json:"message"` +} + +var _ error = &Error{} + +// Error returns the error message. +func (e Error) Error() string { + return fmt.Sprintf("Error (%s): %s", e.Name, e.Message) +} + +func NewError(status int, name string, message string) Error { + return Error{Status: status, Name: name, Message: message} +} + +var ErrUnsupportedMediaType = NewError( + fiber.StatusBadRequest, + UnsupportedMediaType, + "application/json is the only supported media type", +) + +var ErrRouteNotFound = NewError( + fiber.StatusNotFound, + RouteNotFound, + "requested route is not found", +) + +var ErrRouterNotFound = NewError(fiber.StatusNotFound, RouterNotFound, "router is not found") + +var ErrNoModelAvailable = NewError( + 503, + AllModelsUnavailable, + "all providers are unavailable", +) + +func NewPayloadParseErr(err error) Error { + return NewError( + fiber.StatusBadRequest, + PayloadParseError, + err.Error(), + ) +} + +func FromErr(err error) Error { + if apiErr, ok := err.(Error); ok { + return apiErr + } + + return NewError( + fiber.StatusInternalServerError, + UnknownError, + err.Error(), + ) +} diff --git a/pkg/providers/cohere/finish_reason.go b/pkg/providers/cohere/finish_reason.go index 7076a2f3..139498e6 100644 --- a/pkg/providers/cohere/finish_reason.go +++ b/pkg/providers/cohere/finish_reason.go @@ -36,18 +36,18 @@ func (m *FinishReasonMapper) Map(finishReason *string) *schemas.FinishReason { switch strings.ToLower(*finishReason) { case CompleteReason: - reason = &schemas.Complete + reason = &schemas.ReasonComplete case MaxTokensReason: - reason = &schemas.MaxTokens + reason = &schemas.ReasonMaxTokens case FilteredReason: - reason = &schemas.ContentFiltered + reason = &schemas.ReasonContentFiltered default: m.tel.Logger.Warn( "Unknown finish reason, other is going to used", zap.String("unknown_reason", *finishReason), ) - reason = &schemas.OtherReason + reason = &schemas.ReasonOther } return reason diff --git a/pkg/providers/openai/finish_reasons.go b/pkg/providers/openai/finish_reasons.go index 5d2a0fb4..28b5f675 100644 --- a/pkg/providers/openai/finish_reasons.go +++ b/pkg/providers/openai/finish_reasons.go @@ -34,18 +34,18 @@ func (m *FinishReasonMapper) Map(finishReason string) *schemas.FinishReason { switch finishReason { case CompleteReason: - reason = &schemas.Complete + reason = &schemas.ReasonComplete case MaxTokensReason: - reason = &schemas.MaxTokens + reason = &schemas.ReasonMaxTokens case FilteredReason: - reason = &schemas.ContentFiltered + reason = &schemas.ReasonContentFiltered default: m.tel.Logger.Warn( "Unknown finish reason, other is going to used", zap.String("unknown_reason", finishReason), ) - reason = &schemas.OtherReason + reason = &schemas.ReasonOther } return reason diff --git a/pkg/providers/testing/lang.go b/pkg/providers/testing/lang.go index d524dada..e6683aff 100644 --- a/pkg/providers/testing/lang.go +++ b/pkg/providers/testing/lang.go @@ -12,7 +12,7 @@ import ( // RespMock mocks a chat response or a streaming chat chunk type RespMock struct { Msg string - Err *error + Err error } func (m *RespMock) Resp() *schemas.ChatResponse { @@ -81,7 +81,7 @@ func (m *RespStreamMock) Recv() (*schemas.ChatStreamChunk, error) { m.idx++ if chunk.Err != nil { - return nil, *chunk.Err + return nil, chunk.Err } return chunk.RespChunk(), nil @@ -130,7 +130,7 @@ func (c *ProviderMock) Chat(_ context.Context, _ *schemas.ChatRequest) (*schemas c.idx++ if response.Err != nil { - return nil, *response.Err + return nil, response.Err } return response.Resp(), nil diff --git a/pkg/routers/manager.go b/pkg/routers/manager.go index 015616f1..123ea09e 100644 --- a/pkg/routers/manager.go +++ b/pkg/routers/manager.go @@ -1,13 +1,10 @@ package routers import ( - "errors" - + "github.com/EinStack/glide/pkg/api/schemas" "github.com/EinStack/glide/pkg/telemetry" ) -var ErrRouterNotFound = errors.New("no router found with given ID") - type RouterManager struct { Config *Config tel *telemetry.Telemetry @@ -48,5 +45,5 @@ func (r *RouterManager) GetLangRouter(routerID string) (*LangRouter, error) { return router, nil } - return nil, ErrRouterNotFound + return nil, &schemas.ErrRouterNotFound } diff --git a/pkg/routers/router.go b/pkg/routers/router.go index a4128f7d..a069a4fc 100644 --- a/pkg/routers/router.go +++ b/pkg/routers/router.go @@ -16,10 +16,7 @@ import ( "github.com/EinStack/glide/pkg/api/schemas" ) -var ( - ErrNoModels = errors.New("no models configured for router") - ErrNoModelAvailable = errors.New("could not handle request because all providers are not available") -) +var ErrNoModels = errors.New("no models configured for router") type RouterID = string @@ -124,7 +121,7 @@ func (r *LangRouter) Chat(ctx context.Context, req *schemas.ChatRequest) (*schem // if we reach this part, then we are in trouble r.logger.Error("No model was available to handle chat request") - return nil, ErrNoModelAvailable + return nil, schemas.ErrNoModelAvailable } func (r *LangRouter) ChatStream( @@ -139,7 +136,7 @@ func (r *LangRouter) ChatStream( schemas.NoModelConfigured, ErrNoModels.Error(), req.Metadata, - &schemas.ErrorReason, + &schemas.ReasonError, ) return @@ -239,9 +236,9 @@ func (r *LangRouter) ChatStream( respC <- schemas.NewChatStreamError( req.ID, r.routerID, - schemas.AllModelsUnavailable, - ErrNoModelAvailable.Error(), + schemas.ErrNoModelAvailable.Name, + schemas.ErrNoModelAvailable.Message, req.Metadata, - &schemas.ErrorReason, + &schemas.ReasonError, ) } diff --git a/pkg/routers/router_test.go b/pkg/routers/router_test.go index 468aabb0..dc556958 100644 --- a/pkg/routers/router_test.go +++ b/pkg/routers/router_test.go @@ -5,24 +5,15 @@ import ( "testing" "time" - "github.com/EinStack/glide/pkg/routers/latency" - + "github.com/EinStack/glide/pkg/api/schemas" + "github.com/EinStack/glide/pkg/providers" "github.com/EinStack/glide/pkg/providers/clients" - - "github.com/EinStack/glide/pkg/telemetry" - - "github.com/EinStack/glide/pkg/routers/routing" - - "github.com/EinStack/glide/pkg/routers/retry" - - "github.com/EinStack/glide/pkg/routers/health" - ptesting "github.com/EinStack/glide/pkg/providers/testing" - - "github.com/EinStack/glide/pkg/providers" - - "github.com/EinStack/glide/pkg/api/schemas" - + "github.com/EinStack/glide/pkg/routers/health" + "github.com/EinStack/glide/pkg/routers/latency" + "github.com/EinStack/glide/pkg/routers/retry" + "github.com/EinStack/glide/pkg/routers/routing" + "github.com/EinStack/glide/pkg/telemetry" "github.com/stretchr/testify/require" ) @@ -80,14 +71,14 @@ func TestLangRouter_Chat_PickThirdHealthy(t *testing.T) { langModels := []*providers.LanguageModel{ providers.NewLangModel( "first", - ptesting.NewProviderMock([]ptesting.RespMock{{Err: &ErrNoModelAvailable}, {Msg: "3"}}), + ptesting.NewProviderMock([]ptesting.RespMock{{Err: schemas.ErrNoModelAvailable}, {Msg: "3"}}), budget, *latConfig, 1, ), providers.NewLangModel( "second", - ptesting.NewProviderMock([]ptesting.RespMock{{Err: &ErrNoModelAvailable}, {Msg: "4"}}), + ptesting.NewProviderMock([]ptesting.RespMock{{Err: schemas.ErrNoModelAvailable}, {Msg: "4"}}), budget, *latConfig, 1, @@ -138,14 +129,14 @@ func TestLangRouter_Chat_SuccessOnRetry(t *testing.T) { langModels := []*providers.LanguageModel{ providers.NewLangModel( "first", - ptesting.NewProviderMock([]ptesting.RespMock{{Err: &ErrNoModelAvailable}, {Msg: "2"}}), + ptesting.NewProviderMock([]ptesting.RespMock{{Err: &schemas.ErrNoModelAvailable}, {Msg: "2"}}), budget, *latConfig, 1, ), providers.NewLangModel( "second", - ptesting.NewProviderMock([]ptesting.RespMock{{Err: &ErrNoModelAvailable}, {Msg: "1"}}), + ptesting.NewProviderMock([]ptesting.RespMock{{Err: &schemas.ErrNoModelAvailable}, {Msg: "1"}}), budget, *latConfig, 1, @@ -182,7 +173,7 @@ func TestLangRouter_Chat_UnhealthyModelInThePool(t *testing.T) { langModels := []*providers.LanguageModel{ providers.NewLangModel( "first", - ptesting.NewProviderMock([]ptesting.RespMock{{Err: &clients.ErrProviderUnavailable}, {Msg: "3"}}), + ptesting.NewProviderMock([]ptesting.RespMock{{Err: clients.ErrProviderUnavailable}, {Msg: "3"}}), budget, *latConfig, 1, @@ -228,14 +219,14 @@ func TestLangRouter_Chat_AllModelsUnavailable(t *testing.T) { langModels := []*providers.LanguageModel{ providers.NewLangModel( "first", - ptesting.NewProviderMock([]ptesting.RespMock{{Err: &ErrNoModelAvailable}, {Err: &ErrNoModelAvailable}}), + ptesting.NewProviderMock([]ptesting.RespMock{{Err: schemas.ErrNoModelAvailable}, {Err: schemas.ErrNoModelAvailable}}), budget, *latConfig, 1, ), providers.NewLangModel( "second", - ptesting.NewProviderMock([]ptesting.RespMock{{Err: &ErrNoModelAvailable}, {Err: &ErrNoModelAvailable}}), + ptesting.NewProviderMock([]ptesting.RespMock{{Err: schemas.ErrNoModelAvailable}, {Err: schemas.ErrNoModelAvailable}}), budget, *latConfig, 1, @@ -419,7 +410,7 @@ func TestLangRouter_ChatStream_AllModelsUnavailable(t *testing.T) { "first", ptesting.NewStreamProviderMock([]ptesting.RespStreamMock{ ptesting.NewRespStreamMock(&[]ptesting.RespMock{ - {Err: &clients.ErrProviderUnavailable}, + {Err: clients.ErrProviderUnavailable}, }), }), budget, @@ -430,7 +421,7 @@ func TestLangRouter_ChatStream_AllModelsUnavailable(t *testing.T) { "second", ptesting.NewStreamProviderMock([]ptesting.RespStreamMock{ ptesting.NewRespStreamMock(&[]ptesting.RespMock{ - {Err: &clients.ErrProviderUnavailable}, + {Err: clients.ErrProviderUnavailable}, }), }), budget,