From 15e72fe2a7f7f7a1aeb8cdb9b3a29af132dc8697 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Fri, 4 Apr 2025 01:40:09 +0530 Subject: [PATCH 01/24] Addition of error handling in go along with error status --- go/core/error.go | 1 + go/core/status_types.go | 1 + 2 files changed, 2 insertions(+) create mode 100644 go/core/error.go create mode 100644 go/core/status_types.go diff --git a/go/core/error.go b/go/core/error.go new file mode 100644 index 0000000000..9a8bc9592b --- /dev/null +++ b/go/core/error.go @@ -0,0 +1 @@ +package core diff --git a/go/core/status_types.go b/go/core/status_types.go new file mode 100644 index 0000000000..9a8bc9592b --- /dev/null +++ b/go/core/status_types.go @@ -0,0 +1 @@ +package core From 37f73f3c2f7833140e22fa1b2f0f2ed3a1619662 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Fri, 4 Apr 2025 10:11:16 +0530 Subject: [PATCH 02/24] Addition of error handling in go along with error status --- go/ai/generate.go | 2 +- go/core/error.go | 268 +++++++++++++++++++++++++++++++++++++++- go/core/status_types.go | 152 +++++++++++++++++++++++ go/genkit/reflection.go | 31 +---- 4 files changed, 424 insertions(+), 29 deletions(-) diff --git a/go/ai/generate.go b/go/ai/generate.go index 6652d8df8b..631d94358d 100644 --- a/go/ai/generate.go +++ b/go/ai/generate.go @@ -193,7 +193,7 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera maxTurns := opts.MaxTurns if maxTurns < 0 { - return nil, fmt.Errorf("ai.GenerateWithRequest: max turns must be greater than 0, got %d", maxTurns) + return nil, core.NewUserFacingError(core.ABORTED, fmt.Sprintf("ai.GenerateWithRequest: max turns must be greater than 0, got %d", maxTurns), nil) } if maxTurns == 0 { maxTurns = 5 // Default max turns. diff --git a/go/core/error.go b/go/core/error.go index 9a8bc9592b..d1822a3d47 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -1 +1,267 @@ -package core +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// Package core provides base error types and utilities for Genkit. +package core // Or maybe errors? Choose your package name + +import ( + "errors" + "fmt" + "runtime/debug" +) + +type GenkitReflectionAPIDetailsWireFormat struct { + Stack *string `json:"stack,omitempty"` // Use pointer for optional + TraceID *string `json:"traceId,omitempty"` + // Use map[string]any if you need truly arbitrary fields, or embed if known. + // For simplicity here, we only define known fields. Add more needed. +} + +// GenkitReflectionAPIErrorWireFormat is the wire format for HTTP errors. +type GenkitReflectionAPIErrorWireFormat struct { + Details *GenkitReflectionAPIDetailsWireFormat `json:"details,omitempty"` // Pointer to allow nil details + Message string `json:"message"` + Code int `json:"code"` // Defaults handled in creation logic +} + +// HTTPErrorWireFormat is the wire format for HTTP error details for callables. +type HTTPErrorWireFormat struct { + Details any `json:"details,omitempty"` // Use 'any' (interface{}) for arbitrary details + Message string `json:"message"` + Status StatusName `json:"status"` // Use the defined StatusName type +} + +// --- GenkitErrorStruct --- + +// GenkitErrorStruct is the base error type for Genkit errors. +type GenkitErrorStruct struct { + OriginalMessage string `json:"-"` // Exclude from default JSON if embedded elsewhere + Status StatusName `json:"status"` + HTTPCode int `json:"-"` // Exclude from default JSON + Details map[string]any `json:"details"` // Use map for arbitrary details + Source *string `json:"source,omitempty"` // Pointer for optional + TraceID *string `json:"traceId,omitempty"` // Pointer for optional + Cause error `json:"-"` // The underlying wrapped error +} + +// NewGenkitError creates a new GenkitErrorStruct. +func NewGenkitError(msg string, statusName StatusName, cause error, details map[string]any, traceID *string, source *string) *GenkitErrorStruct { + ge := &GenkitErrorStruct{ + OriginalMessage: msg, + Status: statusName, + Source: source, + TraceID: traceID, + Cause: cause, // Store the original cause + Details: details, + } + + // Inherit status from cause if it's a GenkitErrorStruct and status wasn't provided + if ge.Status == "" { // Assuming empty string means not provided + var causeGe *GenkitErrorStruct + if errors.As(cause, &causeGe) { // Check if cause is GenkitErrorStruct + ge.Status = causeGe.Status + } + } + + // Default status if still not set + if ge.Status == "" { + ge.Status = INTERNAL + } + + // Calculate HTTP code + ge.HTTPCode = HTTPStatusCode(ge.Status) + + // Initialize details map if nil + if ge.Details == nil { + ge.Details = make(map[string]any) + } + + // Add stack trace to details if not already present + if _, exists := ge.Details["stack"]; !exists { + // Capture stack trace at the point of error creation + stack := getErrorStack(ge) // Pass the error itself + if stack != "" { + ge.Details["stack"] = stack + } + } + + // Add trace_id to details if not already present and provided + if _, exists := ge.Details["trace_id"]; !exists && traceID != nil { + ge.Details["trace_id"] = *traceID + } + return ge +} + +// Error implements the standard error interface. +func (e *GenkitErrorStruct) Error() string { + sourcePrefix := "" + if e.Source != nil && *e.Source != "" { + sourcePrefix = fmt.Sprintf("%s: ", *e.Source) + } + fmt.Printf("%v", sourcePrefix) + baseMsg := fmt.Sprintf("%s%s: %s", sourcePrefix, e.Status, e.OriginalMessage) + + // Include cause if it exists, using standard wrapping format + if e.Cause != nil { + return fmt.Sprintf("%s: %s", baseMsg, e.Cause.Error()) + } + return baseMsg +} + +// Unwrap provides compatibility with errors.Is and errors.As by returning the wrapped error. +func (e *GenkitErrorStruct) Unwrap() error { + return e.Cause +} + +// ToCallableSerializable returns a JSON-serializable representation for callable responses. +func (e *GenkitErrorStruct) ToCallableSerializable() HTTPErrorWireFormat { + msg := e.OriginalMessage + if e.Cause != nil { + msg = e.Cause.Error() // Similar to repr(cause) - gets the error string + } + return HTTPErrorWireFormat{ + Details: e.Details, + Status: e.Status, // Directly use the status name + Message: msg, + } +} + +// ToSerializable returns a JSON-serializable representation for reflection API responses. +func (e *GenkitErrorStruct) ToSerializable() GenkitReflectionAPIErrorWireFormat { + msg := e.OriginalMessage + if e.Cause != nil { + msg = e.Cause.Error() + } + + detailsWire := &GenkitReflectionAPIDetailsWireFormat{} + // Populate detailsWire from e.Details map + if stackVal, ok := e.Details["stack"].(string); ok { + detailsWire.Stack = &stackVal + } + // Use TraceID field directly if set, otherwise check details map + if e.TraceID != nil { + detailsWire.TraceID = e.TraceID + } else if traceVal, ok := e.Details["trace_id"].(string); ok { + traceIDStr := traceVal // Create a new variable to take its address + detailsWire.TraceID = &traceIDStr + } + + return GenkitReflectionAPIErrorWireFormat{ + Details: detailsWire, + Code: StatusNameToCode[e.Status], // Use the integer code + Message: msg, + } +} + +// --- Specific Error Types (as factory functions) --- + +//// NewUnstableAPIError creates an error for using unstable APIs. +//func NewUnstableAPIError(level string, message *string) *GenkitErrorStruct { +// msgPrefix := "" +// if message != nil && *message != "" { +// msgPrefix = fmt.Sprintf("%s ", *message) +// } +// errMsg := fmt.Sprintf( +// "%sThis API requires '%s' stability level.\n\nTo use this feature, initialize Genkit using `import \"genkit/%s\"`.", // Adjusted import path for Go style +// msgPrefix, +// level, +// level, // Assuming package name matches level e.g., genkit/beta +// ) +// // Note: No cause, details, traceID, source passed here, matching Python version +// return NewGenkitError(errMsg, FAILED_PRECONDITION, nil, nil, nil, nil) +//} + +// NewUserFacingError creates an error suitable for returning to users. +func NewUserFacingError(statusName StatusName, message string, details map[string]any) *GenkitErrorStruct { + // Note: No cause, traceID, source passed here + return NewGenkitError(message, statusName, nil, details, nil, nil) +} + +// --- Utility Functions --- + +//// GetHTTPStatus gets the HTTP status code for an error. +//func GetHTTPStatus(err error) int { +// var ge *GenkitErrorStruct +// if errors.As(err, &ge) { // Check if the error (or any error in its chain) is a GenkitErrorStruct +// return ge.HTTPCode +// } +// return 500 // Default for non-Genkit errors or nil +//} + +// GetReflectionJSON gets the JSON representation for reflection API Error responses. +func GetReflectionJSON(err error) GenkitReflectionAPIErrorWireFormat { + //fmt.Printf("%v", err) + if ge, ok := err.(*GenkitErrorStruct); ok { + return ge.ToSerializable() + } + + // Handle non-Genkit errors + stack := getErrorStack(err) + detailsWire := &GenkitReflectionAPIDetailsWireFormat{} + if stack != "" { + detailsWire.Stack = &stack + } + return GenkitReflectionAPIErrorWireFormat{ + Message: err.Error(), // Use the standard error message + Code: StatusNameToCode[INTERNAL], // Default to INTERNAL code + Details: detailsWire, + } +} + +// +//// GetCallableJSON gets the JSON representation for callable responses. +//func GetCallableJSON(err error) HTTPErrorWireFormat { +// var ge *GenkitErrorStruct +// if errors.As(err, &ge) { +// return ge.ToCallableSerializable() +// } +// +// // Handle non-Genkit errors +// details := make(map[string]any) +// stack := getErrorStack(err) +// if stack != "" { +// details["stack"] = stack +// } +// +// return HTTPErrorWireFormat{ +// Message: err.Error(), // Use the standard error message +// Status: INTERNAL, // Default to INTERNAL status name +// Details: details, // Include stack if available +// } +//} + +//// GetErrorMessage extracts the error message string from any error. +//func GetErrorMessage(err error) string { +// if err == nil { +// return "" +// } +// return err.Error() // The standard Error() method provides this +//} + +// getErrorStack extracts stack trace from an error object. +// This version captures the stack trace of the current goroutine when called. +// It doesn't rely on the error object itself containing the stack, unless +// the error object *is* specifically designed to capture and store it (like we do in NewGenkitErrorStruct). +func getErrorStack(err error) string { + if err == nil { + return "" + } + // Capture the stack trace of the current goroutine. + // Set 'all' false to capture only the current goroutine. + // Note: This reflects the stack *now*, not necessarily the error's origin point, + // unless called immediately at the error site or if stored during creation. + return string(debug.Stack()) +} diff --git a/go/core/status_types.go b/go/core/status_types.go index 9a8bc9592b..cce9d32f7d 100644 --- a/go/core/status_types.go +++ b/go/core/status_types.go @@ -1 +1,153 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// Package status defines canonical status codes, names, and related types +// inspired by gRPC status codes. package core + +import "net/http" // Import standard http package for status codes + +// StatusName defines the set of canonical status names. +type StatusName string + +// Constants for canonical status names. +const ( + OK StatusName = "OK" + CANCELLED StatusName = "CANCELLED" + UNKNOWN StatusName = "UNKNOWN" + INVALID_ARGUMENT StatusName = "INVALID_ARGUMENT" + DEADLINE_EXCEEDED StatusName = "DEADLINE_EXCEEDED" + NOT_FOUND StatusName = "NOT_FOUND" + ALREADY_EXISTS StatusName = "ALREADY_EXISTS" + PERMISSION_DENIED StatusName = "PERMISSION_DENIED" + UNAUTHENTICATED StatusName = "UNAUTHENTICATED" + RESOURCE_EXHAUSTED StatusName = "RESOURCE_EXHAUSTED" + FAILED_PRECONDITION StatusName = "FAILED_PRECONDITION" + ABORTED StatusName = "ABORTED" + OUT_OF_RANGE StatusName = "OUT_OF_RANGE" + UNIMPLEMENTED StatusName = "UNIMPLEMENTED" + INTERNAL StatusName = "INTERNAL_SERVER_ERROR" + UNAVAILABLE StatusName = "UNAVAILABLE" + DATA_LOSS StatusName = "DATA_LOSS" +) + +// Constants for canonical status codes (integer values). +const ( + // CodeOK means not an error; returned on success. + CodeOK = 0 + // CodeCancelled means the operation was cancelled, typically by the caller. + CodeCancelled = 1 + // CodeUnknown means an unknown error occurred. + CodeUnknown = 2 + // CodeInvalidArgument means the client specified an invalid argument. + CodeInvalidArgument = 3 + // CodeDeadlineExceeded means the deadline expired before the operation could complete. + CodeDeadlineExceeded = 4 + // CodeNotFound means some requested entity (e.g., file or directory) was not found. + CodeNotFound = 5 + // CodeAlreadyExists means the entity that a client attempted to create already exists. + CodeAlreadyExists = 6 + // CodePermissionDenied means the caller does not have permission to execute the operation. + CodePermissionDenied = 7 + // CodeUnauthenticated means the request does not have valid authentication credentials. + CodeUnauthenticated = 16 + // CodeResourceExhausted means some resource has been exhausted. + CodeResourceExhausted = 8 + // CodeFailedPrecondition means the operation was rejected because the system is not in a state required. + CodeFailedPrecondition = 9 + // CodeAborted means the operation was aborted, typically due to some issue. + CodeAborted = 10 + // CodeOutOfRange means the operation was attempted past the valid range. + CodeOutOfRange = 11 + // CodeUnimplemented means the operation is not implemented or not supported/enabled. + CodeUnimplemented = 12 + // CodeInternal means internal errors. Some invariants expected by the underlying system were broken. + CodeInternal = 13 + // CodeUnavailable means the service is currently unavailable. + CodeUnavailable = 14 + // CodeDataLoss means unrecoverable data loss or corruption. + CodeDataLoss = 15 +) + +// StatusNameToCode maps status names to their integer code values. +// Exported for potential use elsewhere if needed. +var StatusNameToCode = map[StatusName]int{ + OK: CodeOK, + CANCELLED: CodeCancelled, + UNKNOWN: CodeUnknown, + INVALID_ARGUMENT: CodeInvalidArgument, + DEADLINE_EXCEEDED: CodeDeadlineExceeded, + NOT_FOUND: CodeNotFound, + ALREADY_EXISTS: CodeAlreadyExists, + PERMISSION_DENIED: CodePermissionDenied, + UNAUTHENTICATED: CodeUnauthenticated, + RESOURCE_EXHAUSTED: CodeResourceExhausted, + FAILED_PRECONDITION: CodeFailedPrecondition, + ABORTED: CodeAborted, + OUT_OF_RANGE: CodeOutOfRange, + UNIMPLEMENTED: CodeUnimplemented, + INTERNAL: CodeInternal, + UNAVAILABLE: CodeUnavailable, + DATA_LOSS: CodeDataLoss, +} + +// statusNameToHTTPCode maps status names to HTTP status codes. +// Kept unexported as it's primarily used by the HTTPStatusCode function. +var statusNameToHTTPCode = map[StatusName]int{ + OK: http.StatusOK, // 200 + CANCELLED: 499, // Client Closed Request (non-standard but common) + UNKNOWN: http.StatusInternalServerError, // 500 + INVALID_ARGUMENT: http.StatusBadRequest, // 400 + DEADLINE_EXCEEDED: http.StatusGatewayTimeout, // 504 + NOT_FOUND: http.StatusNotFound, // 404 + ALREADY_EXISTS: http.StatusConflict, // 409 + PERMISSION_DENIED: http.StatusForbidden, // 403 + UNAUTHENTICATED: http.StatusUnauthorized, // 401 + RESOURCE_EXHAUSTED: http.StatusTooManyRequests, // 429 + FAILED_PRECONDITION: http.StatusBadRequest, // 400 + ABORTED: http.StatusConflict, // 409 + OUT_OF_RANGE: http.StatusBadRequest, // 400 + UNIMPLEMENTED: http.StatusNotImplemented, // 501 + INTERNAL: http.StatusInternalServerError, // 500 + UNAVAILABLE: http.StatusServiceUnavailable, // 503 + DATA_LOSS: http.StatusInternalServerError, // 500 +} + +// HTTPStatusCode gets the corresponding HTTP status code for a given Genkit status name. +// It defaults to 500 Internal Server Error if the status name is unrecognized. +func HTTPStatusCode(name StatusName) int { + if code, ok := statusNameToHTTPCode[name]; ok { + return code + } + // Default to 500 if status name is not in the map + return http.StatusInternalServerError +} + +// Status represents a status condition, typically used in responses or errors. +type Status struct { + // Name is the canonical status name. + Name StatusName `json:"name"` + // Message provides an optional developer-facing error message. + Message string `json:"message,omitempty"` // omitempty corresponds to Python's default='' +} + +// NewStatus creates a new Status object. +func NewStatus(name StatusName, message string) *Status { + return &Status{ + Name: name, + Message: message, + } +} diff --git a/go/genkit/reflection.go b/go/genkit/reflection.go index 2bfdefe36a..0d7303e122 100644 --- a/go/genkit/reflection.go +++ b/go/genkit/reflection.go @@ -19,7 +19,6 @@ package genkit import ( "context" "encoding/json" - "errors" "fmt" "log/slog" "net" @@ -250,26 +249,9 @@ func wrapReflectionHandler(h func(w http.ResponseWriter, r *http.Request) error) w.Header().Set("x-genkit-version", "go/"+internal.Version) if err = h(w, r); err != nil { - var traceID string - statusCode := http.StatusInternalServerError - if herr, ok := err.(*base.HTTPError); ok { - traceID = herr.TraceID - statusCode = herr.Code - } - - genkitErr := &ai.GenkitError{ - Message: err.Error(), - Details: struct { - TraceID string `json:"traceId"` - Stack string `json:"stack"` - }{ - TraceID: traceID, - Stack: "", // TODO: Propagate stack trace from local error. - }, - } - - w.WriteHeader(statusCode) - writeJSON(ctx, w, genkitErr) + errorResponse := core.GetReflectionJSON(err) + w.WriteHeader(http.StatusInternalServerError) + writeJSON(ctx, w, errorResponse) } } } @@ -421,12 +403,7 @@ func runAction(ctx context.Context, reg *registry.Registry, key string, input js return action.RunJSON(ctx, input, cb) }) if err != nil { - var herr *base.HTTPError - if errors.As(err, &herr) { - herr.TraceID = traceID - return nil, herr - } - return nil, &base.HTTPError{Code: http.StatusInternalServerError, Err: err, TraceID: traceID} + return nil, core.NewGenkitError("", core.INTERNAL, err, nil, &traceID, nil) } return &runActionResponse{ From 91b75f7bea0bc7934a9f019e078a8728bf11d336 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Fri, 4 Apr 2025 18:12:46 +0530 Subject: [PATCH 03/24] addition of Genkit Error in error.go --- go/ai/generate.go | 51 +++++++++--- go/ai/model_middleware.go | 51 +++++++++--- go/core/error.go | 158 +++++--------------------------------- go/genkit/reflection.go | 11 ++- 4 files changed, 110 insertions(+), 161 deletions(-) diff --git a/go/ai/generate.go b/go/ai/generate.go index 631d94358d..6e84be6534 100644 --- a/go/ai/generate.go +++ b/go/ai/generate.go @@ -139,7 +139,10 @@ func LookupModel(r *registry.Registry, provider, name string) Model { // It returns an error if the model was not defined. func LookupModelByName(r *registry.Registry, modelName string) (Model, error) { if modelName == "" { - return nil, errors.New("ai.LookupModelByName: model not specified") + return nil, &core.GenkitError{ + Message: "ai.LookupModelByName: model not specified", + Status: core.INVALID_ARGUMENT, + } } provider, name, found := strings.Cut(modelName, "/") @@ -151,9 +154,16 @@ func LookupModelByName(r *registry.Registry, modelName string) (Model, error) { model := LookupModel(r, provider, name) if model == nil { if provider == "" { - return nil, fmt.Errorf("ai.LookupModelByName: no model named %q", name) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("ai.LookupModelByName: no model named %q", name), + Status: core.NOT_FOUND, + } + + } + return nil, &core.GenkitError{ + Message: fmt.Sprintf("ai.LookupModelByName: no model named %q for provider %q", name, provider), + Status: core.NOT_FOUND, } - return nil, fmt.Errorf("ai.LookupModelByName: no model named %q for provider %q", name, provider) } return model, nil @@ -164,7 +174,10 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera if opts.Model == "" { opts.Model = r.LookupValue(registry.DefaultModelKey).(string) if opts.Model == "" { - return nil, errors.New("ai.GenerateWithRequest: model is required") + return nil, &core.GenkitError{ + Message: "ai.GenerateWithRequest: model is required", + Status: core.INVALID_ARGUMENT, + } } } @@ -176,12 +189,18 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera toolDefMap := make(map[string]*ToolDefinition) for _, t := range opts.Tools { if _, ok := toolDefMap[t]; ok { - return nil, fmt.Errorf("ai.GenerateWithRequest: duplicate tool found: %q", t) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("ai.GenerateWithRequest: duplicate tool found: %q", t), + Status: core.ALREADY_EXISTS, + } } tool := LookupTool(r, t) if tool == nil { - return nil, fmt.Errorf("ai.GenerateWithRequest: tool not found: %q", t) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("ai.GenerateWithRequest: tool not found: %q", t), + Status: core.NOT_FOUND, + } } toolDefMap[t] = tool.Definition() @@ -193,7 +212,10 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera maxTurns := opts.MaxTurns if maxTurns < 0 { - return nil, core.NewUserFacingError(core.ABORTED, fmt.Sprintf("ai.GenerateWithRequest: max turns must be greater than 0, got %d", maxTurns), nil) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("ai.GenerateWithRequest: max turns must be greater than 0, got %d", maxTurns), + Status: core.ABORTED, + } } if maxTurns == 0 { maxTurns = 5 // Default max turns. @@ -248,7 +270,10 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera } if currentTurn+1 > maxTurns { - return nil, fmt.Errorf("exceeded maximum tool call iterations (%d)", maxTurns) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("exceeded maximum tool call iterations (%d)", maxTurns), + Status: core.ABORTED, + } } newReq, interruptMsg, err := handleToolRequests(ctx, r, req, resp, cb) @@ -276,7 +301,10 @@ func Generate(ctx context.Context, r *registry.Registry, opts ...GenerateOption) for _, opt := range opts { err := opt.applyGenerate(genOpts) if err != nil { - return nil, fmt.Errorf("ai.Generate: error applying options: %w", err) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("ai.Generate: error applying options: %v", err), + Status: core.INVALID_ARGUMENT, + } } } @@ -368,7 +396,10 @@ func (m *modelActionDef) Name() string { return (*ModelAction)(m).Name() } // Generate applies the [Action] to provided request. func (m *modelActionDef) Generate(ctx context.Context, req *ModelRequest, cb ModelStreamCallback) (*ModelResponse, error) { if m == nil { - return nil, errors.New("Model.Generate: generate called on a nil model; check that all models are defined") + return nil, &core.GenkitError{ + Message: "Model.Generate: generate called on a nil model; check that all models are defined", + Status: core.ABORTED, + } } return (*ModelAction)(m).Run(ctx, req, cb) diff --git a/go/ai/model_middleware.go b/go/ai/model_middleware.go index a238972210..3c3b313758 100644 --- a/go/ai/model_middleware.go +++ b/go/ai/model_middleware.go @@ -27,6 +27,7 @@ import ( "strconv" "strings" + "github.com/firebase/genkit/go/core" "github.com/firebase/genkit/go/core/logger" ) @@ -101,28 +102,43 @@ func validateSupport(model string, info *ModelInfo) ModelMiddleware { for _, msg := range input.Messages { for _, part := range msg.Content { if part.IsMedia() { - return nil, fmt.Errorf("model %q does not support media, but media was provided. Request: %+v", model, input) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("model %q does not support media, but media was provided. Request: %+v", model, input), + Status: core.ABORTED, + } } } } } if !info.Supports.Tools && len(input.Tools) > 0 { - return nil, fmt.Errorf("model %q does not support tool use, but tools were provided. Request: %+v", model, input) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("model %q does not support tool use, but tools were provided. Request: %+v", model, input), + Status: core.UNAVAILABLE, + } } if !info.Supports.Multiturn && len(input.Messages) > 1 { - return nil, fmt.Errorf("model %q does not support multiple messages, but %d were provided. Request: %+v", model, len(input.Messages), input) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("model %q does not support multiple messages, but %d were provided. Request: %+v", model, len(input.Messages), input), + Status: core.UNAVAILABLE, + } } if !info.Supports.ToolChoice && input.ToolChoice != "" && input.ToolChoice != ToolChoiceAuto { - return nil, fmt.Errorf("model %q does not support tool choice, but tool choice was provided. Request: %+v", model, input) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("model %q does not support tool choice, but tool choice was provided. Request: %+v", model, input), + Status: core.UNAVAILABLE, + } } if !info.Supports.SystemRole { for _, msg := range input.Messages { if msg.Role == RoleSystem { - return nil, fmt.Errorf("model %q does not support system role, but system role was provided. Request: %+v", model, input) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("model %q does not support system role, but system role was provided. Request: %+v", model, input), + Status: core.UNAVAILABLE, + } } } } @@ -171,14 +187,20 @@ func validateVersion(model string, versions []string, config any) error { version, ok := versionVal.(string) if !ok { - return fmt.Errorf("version must be a string, got %T", versionVal) + return &core.GenkitError{ + Message: fmt.Sprintf("version must be a string, got %T", versionVal), + Status: core.INVALID_ARGUMENT, + } } if slices.Contains(versions, version) { return nil } - return fmt.Errorf("model %q does not support version %q, supported versions: %v", model, version, versions) + return &core.GenkitError{ + Message: fmt.Sprintf("model %q does not support version %q, supported versions: %v", model, version, versions), + Status: core.NOT_FOUND, + } } // ContextItemTemplate is the default item template for context augmentation. @@ -297,13 +319,19 @@ func DownloadRequestMedia(options *DownloadMediaOptions) ModelMiddleware { resp, err := client.Get(mediaUrl) if err != nil { - return nil, fmt.Errorf("HTTP error downloading media %q: %w", mediaUrl, err) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("HTTP error downloading media %q: %v", mediaUrl, err), + Status: core.ABORTED, + } } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("HTTP error downloading media %q: %s", mediaUrl, string(body)) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("HTTP error downloading media %q: %s", mediaUrl, string(body)), + Status: core.ABORTED, + } } contentType := part.ContentType @@ -319,7 +347,10 @@ func DownloadRequestMedia(options *DownloadMediaOptions) ModelMiddleware { data, err = io.ReadAll(resp.Body) } if err != nil { - return nil, fmt.Errorf("error reading media %q: %v", mediaUrl, err) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("error reading media %q: %v", mediaUrl, err), + Status: core.ABORTED, + } } message.Content[j] = NewMediaPart(contentType, fmt.Sprintf("data:%s;base64,%s", contentType, base64.StdEncoding.EncodeToString(data))) diff --git a/go/core/error.go b/go/core/error.go index d1822a3d47..eef8bec4f2 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -15,10 +15,9 @@ // SPDX-License-Identifier: Apache-2.0 // Package core provides base error types and utilities for Genkit. -package core // Or maybe errors? Choose your package name +package core import ( - "errors" "fmt" "runtime/debug" ) @@ -44,94 +43,31 @@ type HTTPErrorWireFormat struct { Status StatusName `json:"status"` // Use the defined StatusName type } -// --- GenkitErrorStruct --- +// --- GenkitError --- -// GenkitErrorStruct is the base error type for Genkit errors. -type GenkitErrorStruct struct { - OriginalMessage string `json:"-"` // Exclude from default JSON if embedded elsewhere - Status StatusName `json:"status"` - HTTPCode int `json:"-"` // Exclude from default JSON - Details map[string]any `json:"details"` // Use map for arbitrary details - Source *string `json:"source,omitempty"` // Pointer for optional - TraceID *string `json:"traceId,omitempty"` // Pointer for optional - Cause error `json:"-"` // The underlying wrapped error -} - -// NewGenkitError creates a new GenkitErrorStruct. -func NewGenkitError(msg string, statusName StatusName, cause error, details map[string]any, traceID *string, source *string) *GenkitErrorStruct { - ge := &GenkitErrorStruct{ - OriginalMessage: msg, - Status: statusName, - Source: source, - TraceID: traceID, - Cause: cause, // Store the original cause - Details: details, - } - - // Inherit status from cause if it's a GenkitErrorStruct and status wasn't provided - if ge.Status == "" { // Assuming empty string means not provided - var causeGe *GenkitErrorStruct - if errors.As(cause, &causeGe) { // Check if cause is GenkitErrorStruct - ge.Status = causeGe.Status - } - } - - // Default status if still not set - if ge.Status == "" { - ge.Status = INTERNAL - } - - // Calculate HTTP code - ge.HTTPCode = HTTPStatusCode(ge.Status) - - // Initialize details map if nil - if ge.Details == nil { - ge.Details = make(map[string]any) - } - - // Add stack trace to details if not already present - if _, exists := ge.Details["stack"]; !exists { - // Capture stack trace at the point of error creation - stack := getErrorStack(ge) // Pass the error itself - if stack != "" { - ge.Details["stack"] = stack - } - } - - // Add trace_id to details if not already present and provided - if _, exists := ge.Details["trace_id"]; !exists && traceID != nil { - ge.Details["trace_id"] = *traceID - } - return ge +// GenkitError is the base error type for Genkit errors. +type GenkitError struct { + Message string `json:"message"` // Exclude from default JSON if embedded elsewhere + Status StatusName `json:"status"` + HTTPCode int `json:"-"` // Exclude from default JSON + Details map[string]any `json:"details"` // Use map for arbitrary details + Source *string `json:"source,omitempty"` // Pointer for optional } // Error implements the standard error interface. -func (e *GenkitErrorStruct) Error() string { +func (e *GenkitError) Error() string { sourcePrefix := "" if e.Source != nil && *e.Source != "" { sourcePrefix = fmt.Sprintf("%s: ", *e.Source) } - fmt.Printf("%v", sourcePrefix) - baseMsg := fmt.Sprintf("%s%s: %s", sourcePrefix, e.Status, e.OriginalMessage) + baseMsg := fmt.Sprintf("%s%s: %s", sourcePrefix, e.Status, e.Message) - // Include cause if it exists, using standard wrapping format - if e.Cause != nil { - return fmt.Sprintf("%s: %s", baseMsg, e.Cause.Error()) - } return baseMsg } -// Unwrap provides compatibility with errors.Is and errors.As by returning the wrapped error. -func (e *GenkitErrorStruct) Unwrap() error { - return e.Cause -} - // ToCallableSerializable returns a JSON-serializable representation for callable responses. -func (e *GenkitErrorStruct) ToCallableSerializable() HTTPErrorWireFormat { - msg := e.OriginalMessage - if e.Cause != nil { - msg = e.Cause.Error() // Similar to repr(cause) - gets the error string - } +func (e *GenkitError) ToCallableSerializable() HTTPErrorWireFormat { + msg := e.Message return HTTPErrorWireFormat{ Details: e.Details, Status: e.Status, // Directly use the status name @@ -140,21 +76,15 @@ func (e *GenkitErrorStruct) ToCallableSerializable() HTTPErrorWireFormat { } // ToSerializable returns a JSON-serializable representation for reflection API responses. -func (e *GenkitErrorStruct) ToSerializable() GenkitReflectionAPIErrorWireFormat { - msg := e.OriginalMessage - if e.Cause != nil { - msg = e.Cause.Error() - } - +func (e *GenkitError) ToSerializable() GenkitReflectionAPIErrorWireFormat { + msg := e.Message detailsWire := &GenkitReflectionAPIDetailsWireFormat{} // Populate detailsWire from e.Details map if stackVal, ok := e.Details["stack"].(string); ok { detailsWire.Stack = &stackVal } // Use TraceID field directly if set, otherwise check details map - if e.TraceID != nil { - detailsWire.TraceID = e.TraceID - } else if traceVal, ok := e.Details["trace_id"].(string); ok { + if traceVal, ok := e.Details["traceId"].(string); ok { traceIDStr := traceVal // Create a new variable to take its address detailsWire.TraceID = &traceIDStr } @@ -166,48 +96,11 @@ func (e *GenkitErrorStruct) ToSerializable() GenkitReflectionAPIErrorWireFormat } } -// --- Specific Error Types (as factory functions) --- - -//// NewUnstableAPIError creates an error for using unstable APIs. -//func NewUnstableAPIError(level string, message *string) *GenkitErrorStruct { -// msgPrefix := "" -// if message != nil && *message != "" { -// msgPrefix = fmt.Sprintf("%s ", *message) -// } -// errMsg := fmt.Sprintf( -// "%sThis API requires '%s' stability level.\n\nTo use this feature, initialize Genkit using `import \"genkit/%s\"`.", // Adjusted import path for Go style -// msgPrefix, -// level, -// level, // Assuming package name matches level e.g., genkit/beta -// ) -// // Note: No cause, details, traceID, source passed here, matching Python version -// return NewGenkitError(errMsg, FAILED_PRECONDITION, nil, nil, nil, nil) -//} - -// NewUserFacingError creates an error suitable for returning to users. -func NewUserFacingError(statusName StatusName, message string, details map[string]any) *GenkitErrorStruct { - // Note: No cause, traceID, source passed here - return NewGenkitError(message, statusName, nil, details, nil, nil) -} - -// --- Utility Functions --- - -//// GetHTTPStatus gets the HTTP status code for an error. -//func GetHTTPStatus(err error) int { -// var ge *GenkitErrorStruct -// if errors.As(err, &ge) { // Check if the error (or any error in its chain) is a GenkitErrorStruct -// return ge.HTTPCode -// } -// return 500 // Default for non-Genkit errors or nil -//} - // GetReflectionJSON gets the JSON representation for reflection API Error responses. func GetReflectionJSON(err error) GenkitReflectionAPIErrorWireFormat { - //fmt.Printf("%v", err) - if ge, ok := err.(*GenkitErrorStruct); ok { + if ge, ok := err.(*GenkitError); ok { return ge.ToSerializable() } - // Handle non-Genkit errors stack := getErrorStack(err) detailsWire := &GenkitReflectionAPIDetailsWireFormat{} @@ -224,7 +117,7 @@ func GetReflectionJSON(err error) GenkitReflectionAPIErrorWireFormat { // //// GetCallableJSON gets the JSON representation for callable responses. //func GetCallableJSON(err error) HTTPErrorWireFormat { -// var ge *GenkitErrorStruct +// var ge *GenkitError // if errors.As(err, &ge) { // return ge.ToCallableSerializable() // } @@ -243,25 +136,12 @@ func GetReflectionJSON(err error) GenkitReflectionAPIErrorWireFormat { // } //} -//// GetErrorMessage extracts the error message string from any error. -//func GetErrorMessage(err error) string { -// if err == nil { -// return "" -// } -// return err.Error() // The standard Error() method provides this -//} - // getErrorStack extracts stack trace from an error object. -// This version captures the stack trace of the current goroutine when called. -// It doesn't rely on the error object itself containing the stack, unless -// the error object *is* specifically designed to capture and store it (like we do in NewGenkitErrorStruct). +// This captures the stack trace of the current goroutine when called. func getErrorStack(err error) string { if err == nil { return "" } // Capture the stack trace of the current goroutine. - // Set 'all' false to capture only the current goroutine. - // Note: This reflects the stack *now*, not necessarily the error's origin point, - // unless called immediately at the error site or if stored during creation. return string(debug.Stack()) } diff --git a/go/genkit/reflection.go b/go/genkit/reflection.go index 0d7303e122..8b5b4209e2 100644 --- a/go/genkit/reflection.go +++ b/go/genkit/reflection.go @@ -269,7 +269,11 @@ func handleRunAction(reg *registry.Registry) func(w http.ResponseWriter, r *http } defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return &base.HTTPError{Code: http.StatusBadRequest, Err: err} + return &core.GenkitError{ + Message: err.Error(), + Status: core.FAILED_PRECONDITION, + } + //return &base.HTTPError{Code: http.StatusBadRequest, Err: err} } stream, err := parseBoolQueryParam(r, "stream") @@ -403,7 +407,10 @@ func runAction(ctx context.Context, reg *registry.Registry, key string, input js return action.RunJSON(ctx, input, cb) }) if err != nil { - return nil, core.NewGenkitError("", core.INTERNAL, err, nil, &traceID, nil) + return nil, &core.GenkitError{ + Message: err.Error(), + Status: core.INTERNAL, + } } return &runActionResponse{ From c17ce9a9ba42536922c38059cef9ee678dab3ac2 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Fri, 4 Apr 2025 19:26:51 +0530 Subject: [PATCH 04/24] fix in error code --- go/core/error.go | 2 +- go/genkit/reflection.go | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index eef8bec4f2..49289ae9b0 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -91,7 +91,7 @@ func (e *GenkitError) ToSerializable() GenkitReflectionAPIErrorWireFormat { return GenkitReflectionAPIErrorWireFormat{ Details: detailsWire, - Code: StatusNameToCode[e.Status], // Use the integer code + Code: HTTPStatusCode(e.Status), // Use the integer code Message: msg, } } diff --git a/go/genkit/reflection.go b/go/genkit/reflection.go index 8b5b4209e2..434ad9ad67 100644 --- a/go/genkit/reflection.go +++ b/go/genkit/reflection.go @@ -250,7 +250,7 @@ func wrapReflectionHandler(h func(w http.ResponseWriter, r *http.Request) error) if err = h(w, r); err != nil { errorResponse := core.GetReflectionJSON(err) - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(errorResponse.Code) writeJSON(ctx, w, errorResponse) } } @@ -394,7 +394,11 @@ type telemetry struct { func runAction(ctx context.Context, reg *registry.Registry, key string, input json.RawMessage, cb streamingCallback[json.RawMessage], runtimeContext map[string]any) (*runActionResponse, error) { action := reg.LookupAction(key) if action == nil { - return nil, &base.HTTPError{Code: http.StatusNotFound, Err: fmt.Errorf("no action with key %q", key)} + return nil, &core.GenkitError{ + Message: fmt.Sprintf("no action with key %q", key), + Status: core.NOT_FOUND, + } + //& base.HTTPError{Code: http.StatusNotFound, Err: fmt.Errorf("no action with key %q", key)} } if runtimeContext != nil { ctx = core.WithActionContext(ctx, runtimeContext) From 12694bf2d6381d37f4224bff2de3afd3231bf113 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Sun, 6 Apr 2025 23:37:10 +0530 Subject: [PATCH 05/24] fix in test case --- go/ai/generate.go | 11 +++++++++-- go/core/action.go | 6 ++++-- go/genkit/reflection.go | 4 +--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/go/ai/generate.go b/go/ai/generate.go index 6e84be6534..f32c175848 100644 --- a/go/ai/generate.go +++ b/go/ai/generate.go @@ -555,7 +555,11 @@ func validResponse(ctx context.Context, resp *ModelResponse) (*Message, error) { msg, err := validMessage(resp.Message, resp.Request.Output) if err != nil { logger.FromContext(ctx).Debug("message did not match expected schema", "error", err.Error()) - return nil, errors.New("generation did not result in a message matching expected schema") + return nil, &core.GenkitError{ + Message: "generation did not result in a message matching expected schema", + Status: core.INVALID_ARGUMENT, + } + //errors.New("generation did not result in a message matching expected schema") } return msg, nil } @@ -584,7 +588,10 @@ func validMessage(m *Message, output *ModelOutputConfig) (*Message, error) { return nil, fmt.Errorf("expected schema is not valid: %w", err) } if err = base.ValidateRaw([]byte(text), schemaBytes); err != nil { - return nil, err + return nil, &core.GenkitError{ + Message: err.Error(), + Status: core.INVALID_ARGUMENT, + } } m.Content[i] = NewJSONPart(text) diff --git a/go/core/action.go b/go/core/action.go index 076a2a7505..ab422bb658 100644 --- a/go/core/action.go +++ b/go/core/action.go @@ -20,7 +20,6 @@ import ( "context" "encoding/json" "fmt" - "net/http" "reflect" "time" @@ -213,7 +212,10 @@ func (a *ActionDef[In, Out, Stream]) Run(ctx context.Context, input In, cb Strea func (a *ActionDef[In, Out, Stream]) RunJSON(ctx context.Context, input json.RawMessage, cb StreamCallback[json.RawMessage]) (json.RawMessage, error) { // Validate input before unmarshaling it because invalid or unknown fields will be discarded in the process. if err := base.ValidateJSON(input, a.inputSchema); err != nil { - return nil, &base.HTTPError{Code: http.StatusBadRequest, Err: err} + return nil, &GenkitError{ + Message: err.Error(), + Status: INVALID_ARGUMENT, + } } var in In if input != nil { diff --git a/go/genkit/reflection.go b/go/genkit/reflection.go index 434ad9ad67..919f570ae9 100644 --- a/go/genkit/reflection.go +++ b/go/genkit/reflection.go @@ -273,7 +273,6 @@ func handleRunAction(reg *registry.Registry) func(w http.ResponseWriter, r *http Message: err.Error(), Status: core.FAILED_PRECONDITION, } - //return &base.HTTPError{Code: http.StatusBadRequest, Err: err} } stream, err := parseBoolQueryParam(r, "stream") @@ -398,7 +397,6 @@ func runAction(ctx context.Context, reg *registry.Registry, key string, input js Message: fmt.Sprintf("no action with key %q", key), Status: core.NOT_FOUND, } - //& base.HTTPError{Code: http.StatusNotFound, Err: fmt.Errorf("no action with key %q", key)} } if runtimeContext != nil { ctx = core.WithActionContext(ctx, runtimeContext) @@ -413,7 +411,7 @@ func runAction(ctx context.Context, reg *registry.Registry, key string, input js if err != nil { return nil, &core.GenkitError{ Message: err.Error(), - Status: core.INTERNAL, + Status: core.INVALID_ARGUMENT, } } From 48e895e1f3a5d1beb63872d72bf9f4a5cb518614 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Mon, 7 Apr 2025 21:34:32 +0530 Subject: [PATCH 06/24] addition of UserError and removal of HTTPError --- go/ai/gen.go | 17 --------------- go/core/error.go | 41 ++++++++++++------------------------- go/genkit/reflection.go | 24 ++++++---------------- go/genkit/servers.go | 20 ++++++++++++------ go/internal/base/misc.go | 13 ------------ go/plugins/firebase/auth.go | 7 +++---- 6 files changed, 36 insertions(+), 86 deletions(-) diff --git a/go/ai/gen.go b/go/ai/gen.go index 865182e731..23acfa8a8d 100644 --- a/go/ai/gen.go +++ b/go/ai/gen.go @@ -172,23 +172,6 @@ type GenerationUsage struct { TotalTokens int `json:"totalTokens,omitempty"` } -type GenkitError struct { - Data *GenkitErrorData `json:"data,omitempty"` - Details any `json:"details,omitempty"` - Message string `json:"message,omitempty"` - Stack string `json:"stack,omitempty"` -} - -type GenkitErrorData struct { - GenkitErrorDetails *GenkitErrorDetails `json:"genkitErrorDetails,omitempty"` - GenkitErrorMessage string `json:"genkitErrorMessage,omitempty"` -} - -type GenkitErrorDetails struct { - Stack string `json:"stack,omitempty"` - TraceID string `json:"traceId,omitempty"` -} - type Media struct { ContentType string `json:"contentType,omitempty"` Url string `json:"url,omitempty"` diff --git a/go/core/error.go b/go/core/error.go index 49289ae9b0..fc931d0ea2 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -25,8 +25,6 @@ import ( type GenkitReflectionAPIDetailsWireFormat struct { Stack *string `json:"stack,omitempty"` // Use pointer for optional TraceID *string `json:"traceId,omitempty"` - // Use map[string]any if you need truly arbitrary fields, or embed if known. - // For simplicity here, we only define known fields. Add more needed. } // GenkitReflectionAPIErrorWireFormat is the wire format for HTTP errors. @@ -43,8 +41,6 @@ type HTTPErrorWireFormat struct { Status StatusName `json:"status"` // Use the defined StatusName type } -// --- GenkitError --- - // GenkitError is the base error type for Genkit errors. type GenkitError struct { Message string `json:"message"` // Exclude from default JSON if embedded elsewhere @@ -70,11 +66,23 @@ func (e *GenkitError) ToCallableSerializable() HTTPErrorWireFormat { msg := e.Message return HTTPErrorWireFormat{ Details: e.Details, - Status: e.Status, // Directly use the status name + Status: e.Status, Message: msg, } } +// UserFacingError error allows a web framework handler to know it +// is safe to return the message in a request. Other kinds of errors will +// result in a generic 500 message to avoid the possibility of internal +// exceptions being leaked to attackers. +func UserFacingError(status StatusName, message string, details map[string]any) *GenkitError { + return &GenkitError{ + Status: status, + Details: details, + Message: message, + } +} + // ToSerializable returns a JSON-serializable representation for reflection API responses. func (e *GenkitError) ToSerializable() GenkitReflectionAPIErrorWireFormat { msg := e.Message @@ -114,34 +122,11 @@ func GetReflectionJSON(err error) GenkitReflectionAPIErrorWireFormat { } } -// -//// GetCallableJSON gets the JSON representation for callable responses. -//func GetCallableJSON(err error) HTTPErrorWireFormat { -// var ge *GenkitError -// if errors.As(err, &ge) { -// return ge.ToCallableSerializable() -// } -// -// // Handle non-Genkit errors -// details := make(map[string]any) -// stack := getErrorStack(err) -// if stack != "" { -// details["stack"] = stack -// } -// -// return HTTPErrorWireFormat{ -// Message: err.Error(), // Use the standard error message -// Status: INTERNAL, // Default to INTERNAL status name -// Details: details, // Include stack if available -// } -//} - // getErrorStack extracts stack trace from an error object. // This captures the stack trace of the current goroutine when called. func getErrorStack(err error) string { if err == nil { return "" } - // Capture the stack trace of the current goroutine. return string(debug.Stack()) } diff --git a/go/genkit/reflection.go b/go/genkit/reflection.go index 7b3084a17f..1132c0965f 100644 --- a/go/genkit/reflection.go +++ b/go/genkit/reflection.go @@ -28,13 +28,11 @@ import ( "strconv" "time" - "github.com/firebase/genkit/go/ai" "github.com/firebase/genkit/go/core" "github.com/firebase/genkit/go/core/logger" "github.com/firebase/genkit/go/core/tracing" "github.com/firebase/genkit/go/internal" "github.com/firebase/genkit/go/internal/action" - "github.com/firebase/genkit/go/internal/base" "github.com/firebase/genkit/go/internal/registry" "go.opentelemetry.io/otel/trace" ) @@ -307,22 +305,9 @@ func handleRunAction(reg *registry.Registry) func(w http.ResponseWriter, r *http resp, err := runAction(ctx, reg, body.Key, body.Input, cb, contextMap) if err != nil { if stream { - var traceID string - if herr, ok := err.(*base.HTTPError); ok { - traceID = herr.TraceID - } - - genkitErr := &ai.GenkitError{ - Message: err.Error(), - Data: &ai.GenkitErrorData{ - GenkitErrorMessage: err.Error(), - GenkitErrorDetails: &ai.GenkitErrorDetails{ - TraceID: traceID, - }, - }, - } - + genkitErr := core.GetReflectionJSON(err) errorJSON, _ := json.Marshal(genkitErr) + fmt.Printf("%v", string(errorJSON)) _, writeErr := fmt.Fprintf(w, "%s\n\n", errorJSON) if writeErr != nil { return writeErr @@ -349,7 +334,10 @@ func handleNotify(reg *registry.Registry) func(w http.ResponseWriter, r *http.Re defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return &base.HTTPError{Code: http.StatusBadRequest, Err: err} + return &core.GenkitError{ + Message: err.Error(), + Status: core.INVALID_ARGUMENT, + } } if os.Getenv("GENKIT_TELEMETRY_SERVER") == "" && body.TelemetryServerURL != "" { diff --git a/go/genkit/servers.go b/go/genkit/servers.go index 59d4bc0a8b..a257ba8f9d 100644 --- a/go/genkit/servers.go +++ b/go/genkit/servers.go @@ -29,7 +29,6 @@ import ( "github.com/firebase/genkit/go/core" "github.com/firebase/genkit/go/core/logger" - "github.com/firebase/genkit/go/internal/base" ) type HandlerOption interface { @@ -90,9 +89,9 @@ func wrapHandler(h func(http.ResponseWriter, *http.Request) error) http.HandlerF }() if err = h(w, r); err != nil { - var herr *base.HTTPError + var herr *core.GenkitError if errors.As(err, &herr) { - http.Error(w, herr.Error(), herr.Code) + http.Error(w, herr.Error(), core.HTTPStatusCode(herr.Status)) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } @@ -113,7 +112,10 @@ func handler(a core.Action, params *handlerParams) func(http.ResponseWriter, *ht if r.Body != nil && r.ContentLength > 0 { defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return &base.HTTPError{Code: http.StatusBadRequest, Err: err} + return &core.GenkitError{ + Message: err.Error(), + Status: core.INVALID_ARGUMENT, + } } } @@ -154,7 +156,10 @@ func handler(a core.Action, params *handlerParams) func(http.ResponseWriter, *ht }) if err != nil { logger.FromContext(ctx).Error("error providing action context from request", "err", err) - return &base.HTTPError{Code: http.StatusUnauthorized, Err: err} + return &core.GenkitError{ + Message: err.Error(), + Status: core.UNAUTHENTICATED, + } } if existing := core.FromContext(ctx); existing != nil { @@ -191,7 +196,10 @@ func parseBoolQueryParam(r *http.Request, name string) (bool, error) { var err error b, err = strconv.ParseBool(s) if err != nil { - return false, &base.HTTPError{Code: http.StatusBadRequest, Err: err} + return false, &core.GenkitError{ + Message: err.Error(), + Status: core.INVALID_ARGUMENT, + } } } return b, nil diff --git a/go/internal/base/misc.go b/go/internal/base/misc.go index ef7e5db6ad..9e3afa1d93 100644 --- a/go/internal/base/misc.go +++ b/go/internal/base/misc.go @@ -17,8 +17,6 @@ package base import ( - "fmt" - "net/http" "net/url" ) @@ -40,14 +38,3 @@ func Zero[T any]() T { func Clean(id string) string { return url.PathEscape(id) } - -// HTTPError is an error that includes an HTTP status code. -type HTTPError struct { - Code int - Err error - TraceID string -} - -func (e *HTTPError) Error() string { - return fmt.Sprintf("%s: %s", http.StatusText(e.Code), e.Err) -} diff --git a/go/plugins/firebase/auth.go b/go/plugins/firebase/auth.go index 833a6e20f6..68ab051883 100644 --- a/go/plugins/firebase/auth.go +++ b/go/plugins/firebase/auth.go @@ -19,7 +19,6 @@ package firebase import ( "context" "encoding/json" - "errors" "fmt" "strings" @@ -53,19 +52,19 @@ func ContextProvider(ctx context.Context, policy AuthPolicy) (core.ContextProvid return func(ctx context.Context, input core.RequestData) (core.ActionContext, error) { authHeader, ok := input.Headers["authorization"] if !ok { - return nil, errors.New("authorization header is required but not provided") + return nil, core.UserFacingError(core.UNAUTHENTICATED, "authorization header is required but not provided", nil) } const bearerPrefix = "bearer " if !strings.HasPrefix(strings.ToLower(authHeader), bearerPrefix) { - return nil, errors.New("invalid authorization header format") + return nil, core.UserFacingError(core.UNAUTHENTICATED, "invalid authorization header format", nil)) } token := authHeader[len(bearerPrefix):] authCtx, err := client.VerifyIDToken(ctx, token) if err != nil { - return nil, fmt.Errorf("error verifying ID token: %v", err) + return nil, core.UserFacingError(core.UNAUTHENTICATED, fmt.Sprintf("error verifying ID token: %v", err), nil) } if policy != nil { From e258baab32a88539bc33c94088a8214ccc79f6e7 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Mon, 7 Apr 2025 21:39:50 +0530 Subject: [PATCH 07/24] removal of Genkit Error from genkit-schema.json as not in used --- genkit-tools/genkit-schema.json | 42 +-------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/genkit-tools/genkit-schema.json b/genkit-tools/genkit-schema.json index 246bdb0003..7fa1e6dde7 100644 --- a/genkit-tools/genkit-schema.json +++ b/genkit-tools/genkit-schema.json @@ -453,46 +453,6 @@ }, "additionalProperties": false }, - "GenkitError": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "stack": { - "type": "string" - }, - "details": {}, - "data": { - "type": "object", - "properties": { - "genkitErrorMessage": { - "type": "string" - }, - "genkitErrorDetails": { - "type": "object", - "properties": { - "stack": { - "type": "string" - }, - "traceId": { - "type": "string" - } - }, - "required": [ - "traceId" - ], - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - "required": [ - "message" - ], - "additionalProperties": false - }, "CandidateError": { "type": "object", "properties": { @@ -1501,4 +1461,4 @@ "additionalProperties": false } } -} \ No newline at end of file +} From bfc3025d0f0ed24ffc8a897b9e25296bc3bc7c5b Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Mon, 7 Apr 2025 21:44:55 +0530 Subject: [PATCH 08/24] removal of GenkitError from schemas.config --- go/core/schemas.config | 1 - 1 file changed, 1 deletion(-) diff --git a/go/core/schemas.config b/go/core/schemas.config index 4f7a621f90..a5e7e02e59 100644 --- a/go/core/schemas.config +++ b/go/core/schemas.config @@ -275,4 +275,3 @@ EmbedResponse omit RetrieverRequest omit RetrieverResponse omit -GenkitErrorDataGenkitErrorDetails name GenkitErrorDetails \ No newline at end of file From 505c97a0eae099d1018516c8c2f0485a3a8b0d22 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Mon, 7 Apr 2025 23:40:09 +0530 Subject: [PATCH 09/24] reverted GenkitError in gen.go to fix the build issue --- genkit-tools/genkit-schema.json | 40 +++++++++++++++++++++++++++++++++ go/ai/gen.go | 17 ++++++++++++++ go/core/schemas.config | 1 + 3 files changed, 58 insertions(+) diff --git a/genkit-tools/genkit-schema.json b/genkit-tools/genkit-schema.json index 7fa1e6dde7..239ccd1304 100644 --- a/genkit-tools/genkit-schema.json +++ b/genkit-tools/genkit-schema.json @@ -453,6 +453,46 @@ }, "additionalProperties": false }, + "GenkitError": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "stack": { + "type": "string" + }, + "details": {}, + "data": { + "type": "object", + "properties": { + "genkitErrorMessage": { + "type": "string" + }, + "genkitErrorDetails": { + "type": "object", + "properties": { + "stack": { + "type": "string" + }, + "traceId": { + "type": "string" + } + }, + "required": [ + "traceId" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "required": [ + "message" + ], + "additionalProperties": false + }, "CandidateError": { "type": "object", "properties": { diff --git a/go/ai/gen.go b/go/ai/gen.go index 23acfa8a8d..865182e731 100644 --- a/go/ai/gen.go +++ b/go/ai/gen.go @@ -172,6 +172,23 @@ type GenerationUsage struct { TotalTokens int `json:"totalTokens,omitempty"` } +type GenkitError struct { + Data *GenkitErrorData `json:"data,omitempty"` + Details any `json:"details,omitempty"` + Message string `json:"message,omitempty"` + Stack string `json:"stack,omitempty"` +} + +type GenkitErrorData struct { + GenkitErrorDetails *GenkitErrorDetails `json:"genkitErrorDetails,omitempty"` + GenkitErrorMessage string `json:"genkitErrorMessage,omitempty"` +} + +type GenkitErrorDetails struct { + Stack string `json:"stack,omitempty"` + TraceID string `json:"traceId,omitempty"` +} + type Media struct { ContentType string `json:"contentType,omitempty"` Url string `json:"url,omitempty"` diff --git a/go/core/schemas.config b/go/core/schemas.config index a5e7e02e59..087a0f7b76 100644 --- a/go/core/schemas.config +++ b/go/core/schemas.config @@ -275,3 +275,4 @@ EmbedResponse omit RetrieverRequest omit RetrieverResponse omit +GenkitErrorDataGenkitErrorDetails name GenkitErrorDetails From f7c98a13358e493497fe0e960dbbdd4b39561b0f Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Mon, 7 Apr 2025 23:45:27 +0530 Subject: [PATCH 10/24] fix auth.go error --- go/plugins/firebase/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/plugins/firebase/auth.go b/go/plugins/firebase/auth.go index 68ab051883..23fa4c77f5 100644 --- a/go/plugins/firebase/auth.go +++ b/go/plugins/firebase/auth.go @@ -58,7 +58,7 @@ func ContextProvider(ctx context.Context, policy AuthPolicy) (core.ContextProvid const bearerPrefix = "bearer " if !strings.HasPrefix(strings.ToLower(authHeader), bearerPrefix) { - return nil, core.UserFacingError(core.UNAUTHENTICATED, "invalid authorization header format", nil)) + return nil, core.UserFacingError(core.UNAUTHENTICATED, "invalid authorization header format", nil) } token := authHeader[len(bearerPrefix):] From 42632b6cec2a1ab3cf83a7d4cbb82b929491d65b Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Tue, 8 Apr 2025 20:06:56 +0530 Subject: [PATCH 11/24] fixing build issue --- genkit-tools/genkit-schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genkit-tools/genkit-schema.json b/genkit-tools/genkit-schema.json index 239ccd1304..246bdb0003 100644 --- a/genkit-tools/genkit-schema.json +++ b/genkit-tools/genkit-schema.json @@ -1501,4 +1501,4 @@ "additionalProperties": false } } -} +} \ No newline at end of file From 1a5eb17067046ba79cdb9cf1fb8414f93001031a Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Mon, 14 Apr 2025 22:52:28 -0700 Subject: [PATCH 12/24] Fixed naming and various status codes. --- go/ai/gen.go | 17 -------- go/ai/generate.go | 12 +++--- go/ai/model_middleware.go | 15 ++++--- go/core/error.go | 84 +++++++++++++++++++-------------------- go/core/schemas.config | 4 +- go/core/status_types.go | 2 +- go/genkit/reflection.go | 26 ++++++------ go/genkit/servers.go | 5 +-- 8 files changed, 75 insertions(+), 90 deletions(-) diff --git a/go/ai/gen.go b/go/ai/gen.go index 1f16227c61..8281084597 100644 --- a/go/ai/gen.go +++ b/go/ai/gen.go @@ -181,23 +181,6 @@ type GenerationUsage struct { TotalTokens int `json:"totalTokens,omitempty"` } -type GenkitError struct { - Data *GenkitErrorData `json:"data,omitempty"` - Details any `json:"details,omitempty"` - Message string `json:"message,omitempty"` - Stack string `json:"stack,omitempty"` -} - -type GenkitErrorData struct { - GenkitErrorDetails *GenkitErrorDetails `json:"genkitErrorDetails,omitempty"` - GenkitErrorMessage string `json:"genkitErrorMessage,omitempty"` -} - -type GenkitErrorDetails struct { - Stack string `json:"stack,omitempty"` - TraceID string `json:"traceId,omitempty"` -} - type Media struct { ContentType string `json:"contentType,omitempty"` Url string `json:"url,omitempty"` diff --git a/go/ai/generate.go b/go/ai/generate.go index c893c3e9e5..546d718195 100644 --- a/go/ai/generate.go +++ b/go/ai/generate.go @@ -169,13 +169,13 @@ func LookupModelByName(r *registry.Registry, modelName string) (Model, error) { if model == nil { if provider == "" { return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.LookupModelByName: no model named %q", name), + Message: fmt.Sprintf("ai.LookupModelByName: model %q not found", name), Status: core.NOT_FOUND, } } return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.LookupModelByName: no model named %q for provider %q", name, provider), + Message: fmt.Sprintf("ai.LookupModelByName: model %q provider %q not found", name, provider), Status: core.NOT_FOUND, } } @@ -207,15 +207,15 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera for _, t := range opts.Tools { if _, ok := toolDefMap[t]; ok { return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.GenerateWithRequest: duplicate tool found: %q", t), - Status: core.ALREADY_EXISTS, + Message: fmt.Sprintf("ai.GenerateWithRequest: duplicate tool %q", t), + Status: core.INVALID_ARGUMENT, } } tool := LookupTool(r, t) if tool == nil { return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.GenerateWithRequest: tool not found: %q", t), + Message: fmt.Sprintf("ai.GenerateWithRequest: tool %q not found", t), Status: core.NOT_FOUND, } } @@ -451,7 +451,7 @@ func (m *model) Generate(ctx context.Context, req *ModelRequest, cb ModelStreamC if m == nil { return nil, &core.GenkitError{ Message: "Model.Generate: generate called on a nil model; check that all models are defined", - Status: core.ABORTED, + Status: core.INVALID_ARGUMENT, } } diff --git a/go/ai/model_middleware.go b/go/ai/model_middleware.go index 6bf40dba32..e585a2028c 100644 --- a/go/ai/model_middleware.go +++ b/go/ai/model_middleware.go @@ -104,7 +104,7 @@ func validateSupport(model string, info *ModelInfo) ModelMiddleware { if part.IsMedia() { return nil, &core.GenkitError{ Message: fmt.Sprintf("model %q does not support media, but media was provided. Request: %+v", model, input), - Status: core.ABORTED, + Status: core.INVALID_ARGUMENT, } } } @@ -114,21 +114,21 @@ func validateSupport(model string, info *ModelInfo) ModelMiddleware { if !info.Supports.Tools && len(input.Tools) > 0 { return nil, &core.GenkitError{ Message: fmt.Sprintf("model %q does not support tool use, but tools were provided. Request: %+v", model, input), - Status: core.UNAVAILABLE, + Status: core.INVALID_ARGUMENT, } } if !info.Supports.Multiturn && len(input.Messages) > 1 { return nil, &core.GenkitError{ Message: fmt.Sprintf("model %q does not support multiple messages, but %d were provided. Request: %+v", model, len(input.Messages), input), - Status: core.UNAVAILABLE, + Status: core.INVALID_ARGUMENT, } } if !info.Supports.ToolChoice && input.ToolChoice != "" && input.ToolChoice != ToolChoiceAuto { return nil, &core.GenkitError{ Message: fmt.Sprintf("model %q does not support tool choice, but tool choice was provided. Request: %+v", model, input), - Status: core.UNAVAILABLE, + Status: core.INVALID_ARGUMENT, } } @@ -137,7 +137,7 @@ func validateSupport(model string, info *ModelInfo) ModelMiddleware { if msg.Role == RoleSystem { return nil, &core.GenkitError{ Message: fmt.Sprintf("model %q does not support system role, but system role was provided. Request: %+v", model, input), - Status: core.UNAVAILABLE, + Status: core.INVALID_ARGUMENT, } } } @@ -156,7 +156,10 @@ func validateSupport(model string, info *ModelInfo) ModelMiddleware { info.Supports.Constrained == ConstrainedSupportNone || (info.Supports.Constrained == ConstrainedSupportNoTools && len(input.Tools) > 0)) && input.Output != nil && input.Output.Constrained { - return nil, fmt.Errorf("model %q does not support native constrained output, but constrained output was requested. Request: %+v", model, input) + return nil, &core.GenkitError{ + Message: fmt.Sprintf("model %q does not support native constrained output, but constrained output was requested. Request: %+v", model, input), + Status: core.INVALID_ARGUMENT, + } } if err := validateVersion(model, info.Versions, input.Config); err != nil { diff --git a/go/core/error.go b/go/core/error.go index fc931d0ea2..4cd2de663f 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -22,23 +22,23 @@ import ( "runtime/debug" ) -type GenkitReflectionAPIDetailsWireFormat struct { +type ReflectionErrorDetails struct { Stack *string `json:"stack,omitempty"` // Use pointer for optional TraceID *string `json:"traceId,omitempty"` } -// GenkitReflectionAPIErrorWireFormat is the wire format for HTTP errors. -type GenkitReflectionAPIErrorWireFormat struct { - Details *GenkitReflectionAPIDetailsWireFormat `json:"details,omitempty"` // Pointer to allow nil details - Message string `json:"message"` - Code int `json:"code"` // Defaults handled in creation logic +// ReflectionError is the wire format for HTTP errors for Reflection API responses. +type ReflectionError struct { + Details *ReflectionErrorDetails `json:"details,omitempty"` + Message string `json:"message"` + Code int `json:"code"` } -// HTTPErrorWireFormat is the wire format for HTTP error details for callables. -type HTTPErrorWireFormat struct { - Details any `json:"details,omitempty"` // Use 'any' (interface{}) for arbitrary details +// HTTPError is the wire format for HTTP error details for callables. +type HTTPError struct { + Details any `json:"details,omitempty"` Message string `json:"message"` - Status StatusName `json:"status"` // Use the defined StatusName type + Status StatusName `json:"status"` } // GenkitError is the base error type for Genkit errors. @@ -50,6 +50,18 @@ type GenkitError struct { Source *string `json:"source,omitempty"` // Pointer for optional } +// UserFacingError allows a web framework handler to know it +// is safe to return the message in a request. Other kinds of errors will +// result in a generic 500 message to avoid the possibility of internal +// exceptions being leaked to attackers. +func UserFacingError(status StatusName, message string, details map[string]any) *GenkitError { + return &GenkitError{ + Status: status, + Details: details, + Message: message, + } +} + // Error implements the standard error interface. func (e *GenkitError) Error() string { sourcePrefix := "" @@ -61,63 +73,51 @@ func (e *GenkitError) Error() string { return baseMsg } -// ToCallableSerializable returns a JSON-serializable representation for callable responses. -func (e *GenkitError) ToCallableSerializable() HTTPErrorWireFormat { +// ToHTTPError returns a JSON-serializable representation for callable responses. +func (e *GenkitError) ToHTTPError() HTTPError { msg := e.Message - return HTTPErrorWireFormat{ + return HTTPError{ Details: e.Details, Status: e.Status, Message: msg, } } -// UserFacingError error allows a web framework handler to know it -// is safe to return the message in a request. Other kinds of errors will -// result in a generic 500 message to avoid the possibility of internal -// exceptions being leaked to attackers. -func UserFacingError(status StatusName, message string, details map[string]any) *GenkitError { - return &GenkitError{ - Status: status, - Details: details, - Message: message, - } -} - -// ToSerializable returns a JSON-serializable representation for reflection API responses. -func (e *GenkitError) ToSerializable() GenkitReflectionAPIErrorWireFormat { +// ToReflectionError returns a JSON-serializable representation for reflection API responses. +func (e *GenkitError) ToReflectionError() ReflectionError { msg := e.Message - detailsWire := &GenkitReflectionAPIDetailsWireFormat{} - // Populate detailsWire from e.Details map + errDetails := &ReflectionErrorDetails{} if stackVal, ok := e.Details["stack"].(string); ok { - detailsWire.Stack = &stackVal + errDetails.Stack = &stackVal } // Use TraceID field directly if set, otherwise check details map if traceVal, ok := e.Details["traceId"].(string); ok { traceIDStr := traceVal // Create a new variable to take its address - detailsWire.TraceID = &traceIDStr + errDetails.TraceID = &traceIDStr } - return GenkitReflectionAPIErrorWireFormat{ - Details: detailsWire, - Code: HTTPStatusCode(e.Status), // Use the integer code + return ReflectionError{ + Details: errDetails, + Code: HTTPStatusCode(e.Status), Message: msg, } } -// GetReflectionJSON gets the JSON representation for reflection API Error responses. -func GetReflectionJSON(err error) GenkitReflectionAPIErrorWireFormat { +// ToReflectionError gets the JSON representation for reflection API Error responses. +func ToReflectionError(err error) ReflectionError { if ge, ok := err.(*GenkitError); ok { - return ge.ToSerializable() + return ge.ToReflectionError() } - // Handle non-Genkit errors + stack := getErrorStack(err) - detailsWire := &GenkitReflectionAPIDetailsWireFormat{} + detailsWire := &ReflectionErrorDetails{} if stack != "" { detailsWire.Stack = &stack } - return GenkitReflectionAPIErrorWireFormat{ - Message: err.Error(), // Use the standard error message - Code: StatusNameToCode[INTERNAL], // Default to INTERNAL code + + return ReflectionError{ + Message: err.Error(), + Code: StatusNameToCode[INTERNAL], Details: detailsWire, } } diff --git a/go/core/schemas.config b/go/core/schemas.config index 42372aaee4..8b6bf69fae 100644 --- a/go/core/schemas.config +++ b/go/core/schemas.config @@ -272,4 +272,6 @@ Score omit Embedding.embedding type []float32 -GenkitErrorDataGenkitErrorDetails name GenkitErrorDetails +GenkitError omit +GenkitErrorData omit +GenkitErrorDataGenkitErrorDetails omit \ No newline at end of file diff --git a/go/core/status_types.go b/go/core/status_types.go index cce9d32f7d..aaa4f1e0a1 100644 --- a/go/core/status_types.go +++ b/go/core/status_types.go @@ -132,7 +132,7 @@ func HTTPStatusCode(name StatusName) int { if code, ok := statusNameToHTTPCode[name]; ok { return code } - // Default to 500 if status name is not in the map + return http.StatusInternalServerError } diff --git a/go/genkit/reflection.go b/go/genkit/reflection.go index 1132c0965f..ab5e913097 100644 --- a/go/genkit/reflection.go +++ b/go/genkit/reflection.go @@ -247,7 +247,7 @@ func wrapReflectionHandler(h func(w http.ResponseWriter, r *http.Request) error) w.Header().Set("x-genkit-version", "go/"+internal.Version) if err = h(w, r); err != nil { - errorResponse := core.GetReflectionJSON(err) + errorResponse := core.ToReflectionError(err) w.WriteHeader(errorResponse.Code) writeJSON(ctx, w, errorResponse) } @@ -269,7 +269,7 @@ func handleRunAction(reg *registry.Registry) func(w http.ResponseWriter, r *http if err := json.NewDecoder(r.Body).Decode(&body); err != nil { return &core.GenkitError{ Message: err.Error(), - Status: core.FAILED_PRECONDITION, + Status: core.INVALID_ARGUMENT, } } @@ -305,13 +305,16 @@ func handleRunAction(reg *registry.Registry) func(w http.ResponseWriter, r *http resp, err := runAction(ctx, reg, body.Key, body.Input, cb, contextMap) if err != nil { if stream { - genkitErr := core.GetReflectionJSON(err) - errorJSON, _ := json.Marshal(genkitErr) - fmt.Printf("%v", string(errorJSON)) - _, writeErr := fmt.Fprintf(w, "%s\n\n", errorJSON) - if writeErr != nil { - return writeErr + reflectErr, err := json.Marshal(core.ToReflectionError(err)) + if err != nil { + return err + } + + _, err = fmt.Fprintf(w, "%s\n\n", reflectErr) + if err != nil { + return err } + if f, ok := w.(http.Flusher); ok { f.Flush() } @@ -382,7 +385,7 @@ func runAction(ctx context.Context, reg *registry.Registry, key string, input js action := reg.LookupAction(key) if action == nil { return nil, &core.GenkitError{ - Message: fmt.Sprintf("no action with key %q", key), + Message: fmt.Sprintf("action %q not found", key), Status: core.NOT_FOUND, } } @@ -397,10 +400,7 @@ func runAction(ctx context.Context, reg *registry.Registry, key string, input js return action.RunJSON(ctx, input, cb) }) if err != nil { - return nil, &core.GenkitError{ - Message: err.Error(), - Status: core.INVALID_ARGUMENT, - } + return nil, err } return &runActionResponse{ diff --git a/go/genkit/servers.go b/go/genkit/servers.go index a257ba8f9d..789b0c6300 100644 --- a/go/genkit/servers.go +++ b/go/genkit/servers.go @@ -156,10 +156,7 @@ func handler(a core.Action, params *handlerParams) func(http.ResponseWriter, *ht }) if err != nil { logger.FromContext(ctx).Error("error providing action context from request", "err", err) - return &core.GenkitError{ - Message: err.Error(), - Status: core.UNAUTHENTICATED, - } + return err } if existing := core.FromContext(ctx); existing != nil { From 4588196ad280a7ba05298a0bb902a7cb8255b015 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Mon, 14 Apr 2025 22:53:17 -0700 Subject: [PATCH 13/24] Update generate.go --- go/ai/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/ai/generate.go b/go/ai/generate.go index 546d718195..c9d8c3db65 100644 --- a/go/ai/generate.go +++ b/go/ai/generate.go @@ -231,7 +231,7 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera if maxTurns < 0 { return nil, &core.GenkitError{ Message: fmt.Sprintf("ai.GenerateWithRequest: max turns must be greater than 0, got %d", maxTurns), - Status: core.ABORTED, + Status: core.INVALID_ARGUMENT, } } if maxTurns == 0 { From 9aa5f6d5682e91d744138b1522aaa8a252cddad6 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Mon, 14 Apr 2025 22:57:50 -0700 Subject: [PATCH 14/24] Update error.go --- go/core/error.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index 4cd2de663f..acdc60bc3d 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -75,31 +75,27 @@ func (e *GenkitError) Error() string { // ToHTTPError returns a JSON-serializable representation for callable responses. func (e *GenkitError) ToHTTPError() HTTPError { - msg := e.Message return HTTPError{ Details: e.Details, Status: e.Status, - Message: msg, + Message: e.Message, } } // ToReflectionError returns a JSON-serializable representation for reflection API responses. func (e *GenkitError) ToReflectionError() ReflectionError { - msg := e.Message errDetails := &ReflectionErrorDetails{} if stackVal, ok := e.Details["stack"].(string); ok { errDetails.Stack = &stackVal } - // Use TraceID field directly if set, otherwise check details map if traceVal, ok := e.Details["traceId"].(string); ok { - traceIDStr := traceVal // Create a new variable to take its address - errDetails.TraceID = &traceIDStr + errDetails.TraceID = &traceVal } return ReflectionError{ Details: errDetails, Code: HTTPStatusCode(e.Status), - Message: msg, + Message: e.Message, } } From 66a043f0be524fdac667cef1aa40f0a3361d2d5e Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Wed, 16 Apr 2025 20:34:50 +0530 Subject: [PATCH 15/24] Addition go NewGenkitError, also refined the debug stack capturing --- go/ai/generate.go | 63 +++++++++------------------------ go/ai/model_middleware.go | 55 ++++++---------------------- go/core/action.go | 5 +-- go/core/error.go | 42 ++++++++++++---------- go/genkit/reflection.go | 15 ++------ go/genkit/servers.go | 10 ++---- go/plugins/firebase/auth.go | 2 +- go/samples/basic-gemini/main.go | 4 +-- 8 files changed, 61 insertions(+), 135 deletions(-) diff --git a/go/ai/generate.go b/go/ai/generate.go index 0ba78d4298..0873f5f44d 100644 --- a/go/ai/generate.go +++ b/go/ai/generate.go @@ -153,10 +153,7 @@ func LookupModel(r *registry.Registry, provider, name string) Model { // It returns an error if the model was not defined. func LookupModelByName(r *registry.Registry, modelName string) (Model, error) { if modelName == "" { - return nil, &core.GenkitError{ - Message: "ai.LookupModelByName: model not specified", - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, "ai.LookupModelByName: model not specified") } provider, name, found := strings.Cut(modelName, "/") @@ -168,16 +165,10 @@ func LookupModelByName(r *registry.Registry, modelName string) (Model, error) { model := LookupModel(r, provider, name) if model == nil { if provider == "" { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.LookupModelByName: model %q not found", name), - Status: core.NOT_FOUND, - } + return nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("ai.LookupModelByName: model %q not found", name)) } - return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.LookupModelByName: model %q provider %q not found", name, provider), - Status: core.NOT_FOUND, - } + return nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("ai.LookupModelByName: model %q provider %q not found", name, provider)) } return model, nil @@ -190,10 +181,7 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera opts.Model = defaultModel } if opts.Model == "" { - return nil, &core.GenkitError{ - Message: "ai.GenerateWithRequest: model is required", - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, "ai.GenerateWithRequest: model is required") } } @@ -206,18 +194,12 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera toolDefMap := make(map[string]*ToolDefinition) for _, t := range opts.Tools { if _, ok := toolDefMap[t]; ok { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.GenerateWithRequest: duplicate tool %q", t), - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("ai.GenerateWithRequest: duplicate tool %q", t)) } tool := LookupTool(r, t) if tool == nil { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.GenerateWithRequest: tool %q not found", t), - Status: core.NOT_FOUND, - } + return nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("ai.GenerateWithRequest: tool %q not found", t)) } toolDefMap[t] = tool.Definition() @@ -229,10 +211,7 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera maxTurns := opts.MaxTurns if maxTurns < 0 { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.GenerateWithRequest: max turns must be greater than 0, got %d", maxTurns), - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("ai.GenerateWithRequest: max turns must be greater than 0, got %d", maxTurns)) } if maxTurns == 0 { maxTurns = 5 // Default max turns. @@ -298,7 +277,8 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera resp.Message, err = formatHandler.ParseMessage(resp.Message) if err != nil { logger.FromContext(ctx).Debug("model failed to generate output matching expected schema", "error", err.Error()) - return nil, fmt.Errorf("model failed to generate output matching expected schema: %w", err) + return nil, core.NewGenkitError(core.INTERNAL, fmt.Sprintf("model failed to generate output matching expected schema: %v", err)) + } } @@ -313,10 +293,7 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera } if currentTurn+1 > maxTurns { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("exceeded maximum tool call iterations (%d)", maxTurns), - Status: core.ABORTED, - } + return nil, core.NewGenkitError(core.ABORTED, fmt.Sprintf("exceeded maximum tool call iterations (%d)", maxTurns)) } newReq, interruptMsg, err := handleToolRequests(ctx, r, req, resp, cb) @@ -343,10 +320,7 @@ func Generate(ctx context.Context, r *registry.Registry, opts ...GenerateOption) genOpts := &generateOptions{} for _, opt := range opts { if err := opt.applyGenerate(genOpts); err != nil { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("ai.Generate: error applying options: %v", err), - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("ai.Generate: error applying options: %v", err)) } } @@ -449,10 +423,7 @@ func (m *model) Name() string { // Generate applies the [Action] to provided request. func (m *model) Generate(ctx context.Context, req *ModelRequest, cb ModelStreamCallback) (*ModelResponse, error) { if m == nil { - return nil, &core.GenkitError{ - Message: "Model.Generate: generate called on a nil model; check that all models are defined", - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, "Model.Generate: generate called on a nil model; check that all models are defined") } return (*core.ActionDef[*ModelRequest, *ModelResponse, *ModelResponseChunk])(m).Run(ctx, req, cb) @@ -509,12 +480,12 @@ func cloneMessage(m *Message) *Message { panic(fmt.Sprintf("failed to marshal message: %v", err)) } - var copy Message - if err := json.Unmarshal(bytes, ©); err != nil { + var msgCopy Message + if err := json.Unmarshal(bytes, &msgCopy); err != nil { panic(fmt.Sprintf("failed to unmarshal message: %v", err)) } - return © + return &msgCopy } // handleToolRequests processes any tool requests in the response, returning @@ -551,7 +522,7 @@ func handleToolRequests(ctx context.Context, r *registry.Registry, req *ModelReq toolReq := p.ToolRequest tool := LookupTool(r, toolReq.Name) if tool == nil { - resultChan <- toolResult{idx, nil, fmt.Errorf("tool %q not found", toolReq.Name)} + resultChan <- toolResult{idx, nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("tool %q not found", toolReq.Name))} return } @@ -569,7 +540,7 @@ func handleToolRequests(ctx context.Context, r *registry.Registry, req *ModelReq resultChan <- toolResult{idx, nil, interruptErr} return } - resultChan <- toolResult{idx, nil, fmt.Errorf("tool %q failed: %w", toolReq.Name, err)} + resultChan <- toolResult{idx, nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("tool %q failed: %v", toolReq.Name, err))} return } diff --git a/go/ai/model_middleware.go b/go/ai/model_middleware.go index e585a2028c..8ce99f6fd1 100644 --- a/go/ai/model_middleware.go +++ b/go/ai/model_middleware.go @@ -102,43 +102,28 @@ func validateSupport(model string, info *ModelInfo) ModelMiddleware { for _, msg := range input.Messages { for _, part := range msg.Content { if part.IsMedia() { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("model %q does not support media, but media was provided. Request: %+v", model, input), - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support media, but media was provided. Request: %+v", model, input)) } } } } if !info.Supports.Tools && len(input.Tools) > 0 { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("model %q does not support tool use, but tools were provided. Request: %+v", model, input), - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support tool use, but tools were provided. Request: %+v", model, input)) } if !info.Supports.Multiturn && len(input.Messages) > 1 { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("model %q does not support multiple messages, but %d were provided. Request: %+v", model, len(input.Messages), input), - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support multiple messages, but %d were provided. Request: %+v", model, len(input.Messages), input)) } if !info.Supports.ToolChoice && input.ToolChoice != "" && input.ToolChoice != ToolChoiceAuto { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("model %q does not support tool choice, but tool choice was provided. Request: %+v", model, input), - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support tool choice, but tool choice was provided. Request: %+v", model, input)) } if !info.Supports.SystemRole { for _, msg := range input.Messages { if msg.Role == RoleSystem { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("model %q does not support system role, but system role was provided. Request: %+v", model, input), - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support system role, but system role was provided. Request: %+v", model, input)) } } } @@ -156,10 +141,7 @@ func validateSupport(model string, info *ModelInfo) ModelMiddleware { info.Supports.Constrained == ConstrainedSupportNone || (info.Supports.Constrained == ConstrainedSupportNoTools && len(input.Tools) > 0)) && input.Output != nil && input.Output.Constrained { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("model %q does not support native constrained output, but constrained output was requested. Request: %+v", model, input), - Status: core.INVALID_ARGUMENT, - } + return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support native constrained output, but constrained output was requested. Request: %+v", model, input)) } if err := validateVersion(model, info.Versions, input.Config); err != nil { @@ -195,20 +177,14 @@ func validateVersion(model string, versions []string, config any) error { version, ok := versionVal.(string) if !ok { - return &core.GenkitError{ - Message: fmt.Sprintf("version must be a string, got %T", versionVal), - Status: core.INVALID_ARGUMENT, - } + return core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("version must be a string, got %T", versionVal)) } if slices.Contains(versions, version) { return nil } - return &core.GenkitError{ - Message: fmt.Sprintf("model %q does not support version %q, supported versions: %v", model, version, versions), - Status: core.NOT_FOUND, - } + return core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support version %q, supported versions: %v", model, version, versions)) } // ContextItemTemplate is the default item template for context augmentation. @@ -327,19 +303,13 @@ func DownloadRequestMedia(options *DownloadMediaOptions) ModelMiddleware { resp, err := client.Get(mediaUrl) if err != nil { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("HTTP error downloading media %q: %v", mediaUrl, err), - Status: core.ABORTED, - } + return nil, core.NewGenkitError(core.ABORTED, fmt.Sprintf("HTTP error downloading media %q: %v", mediaUrl, err)) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) - return nil, &core.GenkitError{ - Message: fmt.Sprintf("HTTP error downloading media %q: %s", mediaUrl, string(body)), - Status: core.ABORTED, - } + return nil, core.NewGenkitError(core.ABORTED, fmt.Sprintf("HTTP error downloading media %q: %s", mediaUrl, string(body))) } contentType := part.ContentType @@ -355,10 +325,7 @@ func DownloadRequestMedia(options *DownloadMediaOptions) ModelMiddleware { data, err = io.ReadAll(resp.Body) } if err != nil { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("error reading media %q: %v", mediaUrl, err), - Status: core.ABORTED, - } + return nil, core.NewGenkitError(core.ABORTED, fmt.Sprintf("error reading media %q: %v", mediaUrl, err)) } message.Content[j] = NewMediaPart(contentType, fmt.Sprintf("data:%s;base64,%s", contentType, base64.StdEncoding.EncodeToString(data))) diff --git a/go/core/action.go b/go/core/action.go index ab422bb658..35f91cfb1d 100644 --- a/go/core/action.go +++ b/go/core/action.go @@ -212,10 +212,7 @@ func (a *ActionDef[In, Out, Stream]) Run(ctx context.Context, input In, cb Strea func (a *ActionDef[In, Out, Stream]) RunJSON(ctx context.Context, input json.RawMessage, cb StreamCallback[json.RawMessage]) (json.RawMessage, error) { // Validate input before unmarshaling it because invalid or unknown fields will be discarded in the process. if err := base.ValidateJSON(input, a.inputSchema); err != nil { - return nil, &GenkitError{ - Message: err.Error(), - Status: INVALID_ARGUMENT, - } + return nil, NewGenkitError(INVALID_ARGUMENT, err.Error()) } var in In if input != nil { diff --git a/go/core/error.go b/go/core/error.go index acdc60bc3d..2dd02aba93 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -34,13 +34,6 @@ type ReflectionError struct { Code int `json:"code"` } -// HTTPError is the wire format for HTTP error details for callables. -type HTTPError struct { - Details any `json:"details,omitempty"` - Message string `json:"message"` - Status StatusName `json:"status"` -} - // GenkitError is the base error type for Genkit errors. type GenkitError struct { Message string `json:"message"` // Exclude from default JSON if embedded elsewhere @@ -62,6 +55,29 @@ func UserFacingError(status StatusName, message string, details map[string]any) } } +func NewGenkitError(status StatusName, message string) *GenkitError { + ge := &GenkitError{ + Status: status, + Message: message, + } + + stackExists := false + if ge.Details != nil { + _, stackExists = ge.Details["stack"] + } + + if !stackExists { + errStack := getErrorStack(ge) + if errStack != "" { + if ge.Details == nil { + ge.Details = make(map[string]any) + } + ge.Details["stack"] = errStack + } + } + return ge +} + // Error implements the standard error interface. func (e *GenkitError) Error() string { sourcePrefix := "" @@ -73,15 +89,6 @@ func (e *GenkitError) Error() string { return baseMsg } -// ToHTTPError returns a JSON-serializable representation for callable responses. -func (e *GenkitError) ToHTTPError() HTTPError { - return HTTPError{ - Details: e.Details, - Status: e.Status, - Message: e.Message, - } -} - // ToReflectionError returns a JSON-serializable representation for reflection API responses. func (e *GenkitError) ToReflectionError() ReflectionError { errDetails := &ReflectionErrorDetails{} @@ -91,7 +98,6 @@ func (e *GenkitError) ToReflectionError() ReflectionError { if traceVal, ok := e.Details["traceId"].(string); ok { errDetails.TraceID = &traceVal } - return ReflectionError{ Details: errDetails, Code: HTTPStatusCode(e.Status), @@ -113,7 +119,7 @@ func ToReflectionError(err error) ReflectionError { return ReflectionError{ Message: err.Error(), - Code: StatusNameToCode[INTERNAL], + Code: HTTPStatusCode(INTERNAL), Details: detailsWire, } } diff --git a/go/genkit/reflection.go b/go/genkit/reflection.go index ab5e913097..eee987bdc3 100644 --- a/go/genkit/reflection.go +++ b/go/genkit/reflection.go @@ -267,10 +267,7 @@ func handleRunAction(reg *registry.Registry) func(w http.ResponseWriter, r *http } defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return &core.GenkitError{ - Message: err.Error(), - Status: core.INVALID_ARGUMENT, - } + return core.NewGenkitError(core.INVALID_ARGUMENT, err.Error()) } stream, err := parseBoolQueryParam(r, "stream") @@ -337,10 +334,7 @@ func handleNotify(reg *registry.Registry) func(w http.ResponseWriter, r *http.Re defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return &core.GenkitError{ - Message: err.Error(), - Status: core.INVALID_ARGUMENT, - } + return core.NewGenkitError(core.INVALID_ARGUMENT, err.Error()) } if os.Getenv("GENKIT_TELEMETRY_SERVER") == "" && body.TelemetryServerURL != "" { @@ -384,10 +378,7 @@ type telemetry struct { func runAction(ctx context.Context, reg *registry.Registry, key string, input json.RawMessage, cb streamingCallback[json.RawMessage], runtimeContext map[string]any) (*runActionResponse, error) { action := reg.LookupAction(key) if action == nil { - return nil, &core.GenkitError{ - Message: fmt.Sprintf("action %q not found", key), - Status: core.NOT_FOUND, - } + return nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("action %q not found", key)) } if runtimeContext != nil { ctx = core.WithActionContext(ctx, runtimeContext) diff --git a/go/genkit/servers.go b/go/genkit/servers.go index 789b0c6300..ea67420d06 100644 --- a/go/genkit/servers.go +++ b/go/genkit/servers.go @@ -112,10 +112,7 @@ func handler(a core.Action, params *handlerParams) func(http.ResponseWriter, *ht if r.Body != nil && r.ContentLength > 0 { defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return &core.GenkitError{ - Message: err.Error(), - Status: core.INVALID_ARGUMENT, - } + return core.NewGenkitError(core.INVALID_ARGUMENT, err.Error()) } } @@ -193,10 +190,7 @@ func parseBoolQueryParam(r *http.Request, name string) (bool, error) { var err error b, err = strconv.ParseBool(s) if err != nil { - return false, &core.GenkitError{ - Message: err.Error(), - Status: core.INVALID_ARGUMENT, - } + return false, core.NewGenkitError(core.INVALID_ARGUMENT, err.Error()) } } return b, nil diff --git a/go/plugins/firebase/auth.go b/go/plugins/firebase/auth.go index 8cd2a7b579..e3f6d7e270 100644 --- a/go/plugins/firebase/auth.go +++ b/go/plugins/firebase/auth.go @@ -42,7 +42,7 @@ type AuthClient interface { func ContextProvider(ctx context.Context, g *genkit.Genkit, policy AuthPolicy) (core.ContextProvider, error) { f, ok := genkit.LookupPlugin(g, provider).(*Firebase) if !ok { - return nil, errors.New("firebase plugin not initialized; did you pass the plugin to genkit.Init()") + return nil, core.NewGenkitError(core.NOT_FOUND, "firebase plugin not initialized; did you pass the plugin to genkit.Init()") } client, err := f.App.Auth(ctx) if err != nil { diff --git a/go/samples/basic-gemini/main.go b/go/samples/basic-gemini/main.go index dceb064dcc..c9ff720529 100644 --- a/go/samples/basic-gemini/main.go +++ b/go/samples/basic-gemini/main.go @@ -16,10 +16,10 @@ package main import ( "context" - "errors" "log" "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/core" "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/googlegenai" ) @@ -40,7 +40,7 @@ func main() { genkit.DefineFlow(g, "jokesFlow", func(ctx context.Context, input string) (string, error) { m := googlegenai.GoogleAIModel(g, "gemini-2.5-pro-preview-03-25") if m == nil { - return "", errors.New("jokesFlow: failed to find model") + return "", core.NewGenkitError(core.INVALID_ARGUMENT, "jokesFlow: failed to find model") } resp, err := genkit.Generate(ctx, g, From 8c39f0a1c3d8b2e014a3e0e36ba0f59f5070b41a Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Wed, 16 Apr 2025 22:02:24 +0530 Subject: [PATCH 16/24] change in newgenkit error debug stack handling --- go/core/error.go | 17 ++++------------- go/core/status_types.go | 7 ++----- go/genkit/servers.go | 4 ++-- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index 2dd02aba93..8aee14abe7 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -61,19 +61,10 @@ func NewGenkitError(status StatusName, message string) *GenkitError { Message: message, } - stackExists := false - if ge.Details != nil { - _, stackExists = ge.Details["stack"] - } - - if !stackExists { - errStack := getErrorStack(ge) - if errStack != "" { - if ge.Details == nil { - ge.Details = make(map[string]any) - } - ge.Details["stack"] = errStack - } + errStack := getErrorStack(ge) + if errStack != "" { + ge.Details = make(map[string]any) + ge.Details["stack"] = errStack } return ge } diff --git a/go/core/status_types.go b/go/core/status_types.go index aaa4f1e0a1..97a9592a76 100644 --- a/go/core/status_types.go +++ b/go/core/status_types.go @@ -127,7 +127,6 @@ var statusNameToHTTPCode = map[StatusName]int{ } // HTTPStatusCode gets the corresponding HTTP status code for a given Genkit status name. -// It defaults to 500 Internal Server Error if the status name is unrecognized. func HTTPStatusCode(name StatusName) int { if code, ok := statusNameToHTTPCode[name]; ok { return code @@ -138,10 +137,8 @@ func HTTPStatusCode(name StatusName) int { // Status represents a status condition, typically used in responses or errors. type Status struct { - // Name is the canonical status name. - Name StatusName `json:"name"` - // Message provides an optional developer-facing error message. - Message string `json:"message,omitempty"` // omitempty corresponds to Python's default='' + Name StatusName `json:"name"` + Message string `json:"message,omitempty"` } // NewStatus creates a new Status object. diff --git a/go/genkit/servers.go b/go/genkit/servers.go index ea67420d06..62c5ffb739 100644 --- a/go/genkit/servers.go +++ b/go/genkit/servers.go @@ -112,7 +112,7 @@ func handler(a core.Action, params *handlerParams) func(http.ResponseWriter, *ht if r.Body != nil && r.ContentLength > 0 { defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return core.NewGenkitError(core.INVALID_ARGUMENT, err.Error()) + return core.UserFacingError(core.INVALID_ARGUMENT, err.Error(), nil) } } @@ -190,7 +190,7 @@ func parseBoolQueryParam(r *http.Request, name string) (bool, error) { var err error b, err = strconv.ParseBool(s) if err != nil { - return false, core.NewGenkitError(core.INVALID_ARGUMENT, err.Error()) + return false, core.UserFacingError(core.INVALID_ARGUMENT, err.Error(), nil) } } return b, nil From 6015519e7bed6facca85ba2424993f5474a99b5d Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Wed, 16 Apr 2025 13:52:15 -0700 Subject: [PATCH 17/24] Renamed error functions. --- go/ai/generate.go | 27 +++++++++++++-------------- go/ai/model_middleware.go | 22 +++++++++++----------- go/core/action.go | 2 +- go/core/error.go | 12 ++++++++---- go/genkit/reflection.go | 6 +++--- go/genkit/servers.go | 4 ++-- go/plugins/firebase/auth.go | 8 ++++---- go/plugins/mcp/mcp.go | 3 +++ go/samples/basic-gemini/main.go | 2 +- 9 files changed, 46 insertions(+), 40 deletions(-) create mode 100644 go/plugins/mcp/mcp.go diff --git a/go/ai/generate.go b/go/ai/generate.go index 0873f5f44d..f8a8768193 100644 --- a/go/ai/generate.go +++ b/go/ai/generate.go @@ -153,7 +153,7 @@ func LookupModel(r *registry.Registry, provider, name string) Model { // It returns an error if the model was not defined. func LookupModelByName(r *registry.Registry, modelName string) (Model, error) { if modelName == "" { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, "ai.LookupModelByName: model not specified") + return nil, core.NewError(core.INVALID_ARGUMENT, "ai.LookupModelByName: model not specified") } provider, name, found := strings.Cut(modelName, "/") @@ -165,10 +165,9 @@ func LookupModelByName(r *registry.Registry, modelName string) (Model, error) { model := LookupModel(r, provider, name) if model == nil { if provider == "" { - return nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("ai.LookupModelByName: model %q not found", name)) - + return nil, core.NewError(core.NOT_FOUND, "ai.LookupModelByName: model %q not found", name) } - return nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("ai.LookupModelByName: model %q provider %q not found", name, provider)) + return nil, core.NewError(core.NOT_FOUND, "ai.LookupModelByName: model %q by provider %q not found", name, provider) } return model, nil @@ -181,7 +180,7 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera opts.Model = defaultModel } if opts.Model == "" { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, "ai.GenerateWithRequest: model is required") + return nil, core.NewError(core.INVALID_ARGUMENT, "ai.GenerateWithRequest: model is required") } } @@ -194,12 +193,12 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera toolDefMap := make(map[string]*ToolDefinition) for _, t := range opts.Tools { if _, ok := toolDefMap[t]; ok { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("ai.GenerateWithRequest: duplicate tool %q", t)) + return nil, core.NewError(core.INVALID_ARGUMENT, "ai.GenerateWithRequest: duplicate tool %q", t) } tool := LookupTool(r, t) if tool == nil { - return nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("ai.GenerateWithRequest: tool %q not found", t)) + return nil, core.NewError(core.NOT_FOUND, "ai.GenerateWithRequest: tool %q not found", t) } toolDefMap[t] = tool.Definition() @@ -211,7 +210,7 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera maxTurns := opts.MaxTurns if maxTurns < 0 { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("ai.GenerateWithRequest: max turns must be greater than 0, got %d", maxTurns)) + return nil, core.NewError(core.INVALID_ARGUMENT, "ai.GenerateWithRequest: max turns must be greater than 0, got %d", maxTurns) } if maxTurns == 0 { maxTurns = 5 // Default max turns. @@ -277,7 +276,7 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera resp.Message, err = formatHandler.ParseMessage(resp.Message) if err != nil { logger.FromContext(ctx).Debug("model failed to generate output matching expected schema", "error", err.Error()) - return nil, core.NewGenkitError(core.INTERNAL, fmt.Sprintf("model failed to generate output matching expected schema: %v", err)) + return nil, core.NewError(core.INTERNAL, "model failed to generate output matching expected schema: %v", err) } } @@ -293,7 +292,7 @@ func GenerateWithRequest(ctx context.Context, r *registry.Registry, opts *Genera } if currentTurn+1 > maxTurns { - return nil, core.NewGenkitError(core.ABORTED, fmt.Sprintf("exceeded maximum tool call iterations (%d)", maxTurns)) + return nil, core.NewError(core.ABORTED, "exceeded maximum tool call iterations (%d)", maxTurns) } newReq, interruptMsg, err := handleToolRequests(ctx, r, req, resp, cb) @@ -320,7 +319,7 @@ func Generate(ctx context.Context, r *registry.Registry, opts ...GenerateOption) genOpts := &generateOptions{} for _, opt := range opts { if err := opt.applyGenerate(genOpts); err != nil { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("ai.Generate: error applying options: %v", err)) + return nil, core.NewError(core.INVALID_ARGUMENT, "ai.Generate: error applying options: %v", err) } } @@ -423,7 +422,7 @@ func (m *model) Name() string { // Generate applies the [Action] to provided request. func (m *model) Generate(ctx context.Context, req *ModelRequest, cb ModelStreamCallback) (*ModelResponse, error) { if m == nil { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, "Model.Generate: generate called on a nil model; check that all models are defined") + return nil, core.NewError(core.INVALID_ARGUMENT, "Model.Generate: generate called on a nil model; check that all models are defined") } return (*core.ActionDef[*ModelRequest, *ModelResponse, *ModelResponseChunk])(m).Run(ctx, req, cb) @@ -522,7 +521,7 @@ func handleToolRequests(ctx context.Context, r *registry.Registry, req *ModelReq toolReq := p.ToolRequest tool := LookupTool(r, toolReq.Name) if tool == nil { - resultChan <- toolResult{idx, nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("tool %q not found", toolReq.Name))} + resultChan <- toolResult{idx, nil, core.NewError(core.NOT_FOUND, "tool %q not found", toolReq.Name)} return } @@ -540,7 +539,7 @@ func handleToolRequests(ctx context.Context, r *registry.Registry, req *ModelReq resultChan <- toolResult{idx, nil, interruptErr} return } - resultChan <- toolResult{idx, nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("tool %q failed: %v", toolReq.Name, err))} + resultChan <- toolResult{idx, nil, core.NewError(core.INTERNAL, "tool %q failed: %v", toolReq.Name, err)} return } diff --git a/go/ai/model_middleware.go b/go/ai/model_middleware.go index 8ce99f6fd1..76f4610f1e 100644 --- a/go/ai/model_middleware.go +++ b/go/ai/model_middleware.go @@ -102,28 +102,28 @@ func validateSupport(model string, info *ModelInfo) ModelMiddleware { for _, msg := range input.Messages { for _, part := range msg.Content { if part.IsMedia() { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support media, but media was provided. Request: %+v", model, input)) + return nil, core.NewError(core.INVALID_ARGUMENT, "model %q does not support media, but media was provided. Request: %+v", model, input) } } } } if !info.Supports.Tools && len(input.Tools) > 0 { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support tool use, but tools were provided. Request: %+v", model, input)) + return nil, core.NewError(core.INVALID_ARGUMENT, "model %q does not support tool use, but tools were provided. Request: %+v", model, input) } if !info.Supports.Multiturn && len(input.Messages) > 1 { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support multiple messages, but %d were provided. Request: %+v", model, len(input.Messages), input)) + return nil, core.NewError(core.INVALID_ARGUMENT, "model %q does not support multiple messages, but %d were provided. Request: %+v", model, len(input.Messages), input) } if !info.Supports.ToolChoice && input.ToolChoice != "" && input.ToolChoice != ToolChoiceAuto { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support tool choice, but tool choice was provided. Request: %+v", model, input)) + return nil, core.NewError(core.INVALID_ARGUMENT, "model %q does not support tool choice, but tool choice was provided. Request: %+v", model, input) } if !info.Supports.SystemRole { for _, msg := range input.Messages { if msg.Role == RoleSystem { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support system role, but system role was provided. Request: %+v", model, input)) + return nil, core.NewError(core.INVALID_ARGUMENT, "model %q does not support system role, but system role was provided. Request: %+v", model, input) } } } @@ -141,7 +141,7 @@ func validateSupport(model string, info *ModelInfo) ModelMiddleware { info.Supports.Constrained == ConstrainedSupportNone || (info.Supports.Constrained == ConstrainedSupportNoTools && len(input.Tools) > 0)) && input.Output != nil && input.Output.Constrained { - return nil, core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support native constrained output, but constrained output was requested. Request: %+v", model, input)) + return nil, core.NewError(core.INVALID_ARGUMENT, "model %q does not support native constrained output, but constrained output was requested. Request: %+v", model, input) } if err := validateVersion(model, info.Versions, input.Config); err != nil { @@ -177,14 +177,14 @@ func validateVersion(model string, versions []string, config any) error { version, ok := versionVal.(string) if !ok { - return core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("version must be a string, got %T", versionVal)) + return core.NewError(core.INVALID_ARGUMENT, "version must be a string, got %T", versionVal) } if slices.Contains(versions, version) { return nil } - return core.NewGenkitError(core.INVALID_ARGUMENT, fmt.Sprintf("model %q does not support version %q, supported versions: %v", model, version, versions)) + return core.NewError(core.INVALID_ARGUMENT, "model %q does not support version %q, supported versions: %v", model, version, versions) } // ContextItemTemplate is the default item template for context augmentation. @@ -303,13 +303,13 @@ func DownloadRequestMedia(options *DownloadMediaOptions) ModelMiddleware { resp, err := client.Get(mediaUrl) if err != nil { - return nil, core.NewGenkitError(core.ABORTED, fmt.Sprintf("HTTP error downloading media %q: %v", mediaUrl, err)) + return nil, core.NewError(core.INVALID_ARGUMENT, "HTTP error downloading media %q: %v", mediaUrl, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) - return nil, core.NewGenkitError(core.ABORTED, fmt.Sprintf("HTTP error downloading media %q: %s", mediaUrl, string(body))) + return nil, core.NewError(core.UNKNOWN, "HTTP error downloading media %q: %s", mediaUrl, string(body)) } contentType := part.ContentType @@ -325,7 +325,7 @@ func DownloadRequestMedia(options *DownloadMediaOptions) ModelMiddleware { data, err = io.ReadAll(resp.Body) } if err != nil { - return nil, core.NewGenkitError(core.ABORTED, fmt.Sprintf("error reading media %q: %v", mediaUrl, err)) + return nil, core.NewError(core.UNKNOWN, "error reading media %q: %v", mediaUrl, err) } message.Content[j] = NewMediaPart(contentType, fmt.Sprintf("data:%s;base64,%s", contentType, base64.StdEncoding.EncodeToString(data))) diff --git a/go/core/action.go b/go/core/action.go index 35f91cfb1d..a5cd85f855 100644 --- a/go/core/action.go +++ b/go/core/action.go @@ -212,7 +212,7 @@ func (a *ActionDef[In, Out, Stream]) Run(ctx context.Context, input In, cb Strea func (a *ActionDef[In, Out, Stream]) RunJSON(ctx context.Context, input json.RawMessage, cb StreamCallback[json.RawMessage]) (json.RawMessage, error) { // Validate input before unmarshaling it because invalid or unknown fields will be discarded in the process. if err := base.ValidateJSON(input, a.inputSchema); err != nil { - return nil, NewGenkitError(INVALID_ARGUMENT, err.Error()) + return nil, NewError(INVALID_ARGUMENT, err.Error()) } var in In if input != nil { diff --git a/go/core/error.go b/go/core/error.go index 8aee14abe7..9ec88e52c0 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -43,11 +43,11 @@ type GenkitError struct { Source *string `json:"source,omitempty"` // Pointer for optional } -// UserFacingError allows a web framework handler to know it +// NewPublicError allows a web framework handler to know it // is safe to return the message in a request. Other kinds of errors will // result in a generic 500 message to avoid the possibility of internal // exceptions being leaked to attackers. -func UserFacingError(status StatusName, message string, details map[string]any) *GenkitError { +func NewPublicError(status StatusName, message string, details map[string]any) *GenkitError { return &GenkitError{ Status: status, Details: details, @@ -55,10 +55,14 @@ func UserFacingError(status StatusName, message string, details map[string]any) } } -func NewGenkitError(status StatusName, message string) *GenkitError { +// NewError creates a new GenkitError with a stack trace. +func NewError(status StatusName, message string, args ...any) *GenkitError { + // Prevents a compile-time warning about non-constant message. + msg := message + ge := &GenkitError{ Status: status, - Message: message, + Message: fmt.Sprintf(msg, args...), } errStack := getErrorStack(ge) diff --git a/go/genkit/reflection.go b/go/genkit/reflection.go index eee987bdc3..69248b7925 100644 --- a/go/genkit/reflection.go +++ b/go/genkit/reflection.go @@ -267,7 +267,7 @@ func handleRunAction(reg *registry.Registry) func(w http.ResponseWriter, r *http } defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return core.NewGenkitError(core.INVALID_ARGUMENT, err.Error()) + return core.NewError(core.INVALID_ARGUMENT, err.Error()) } stream, err := parseBoolQueryParam(r, "stream") @@ -334,7 +334,7 @@ func handleNotify(reg *registry.Registry) func(w http.ResponseWriter, r *http.Re defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return core.NewGenkitError(core.INVALID_ARGUMENT, err.Error()) + return core.NewError(core.INVALID_ARGUMENT, err.Error()) } if os.Getenv("GENKIT_TELEMETRY_SERVER") == "" && body.TelemetryServerURL != "" { @@ -378,7 +378,7 @@ type telemetry struct { func runAction(ctx context.Context, reg *registry.Registry, key string, input json.RawMessage, cb streamingCallback[json.RawMessage], runtimeContext map[string]any) (*runActionResponse, error) { action := reg.LookupAction(key) if action == nil { - return nil, core.NewGenkitError(core.NOT_FOUND, fmt.Sprintf("action %q not found", key)) + return nil, core.NewError(core.NOT_FOUND, "action %q not found", key) } if runtimeContext != nil { ctx = core.WithActionContext(ctx, runtimeContext) diff --git a/go/genkit/servers.go b/go/genkit/servers.go index 62c5ffb739..c74f673358 100644 --- a/go/genkit/servers.go +++ b/go/genkit/servers.go @@ -112,7 +112,7 @@ func handler(a core.Action, params *handlerParams) func(http.ResponseWriter, *ht if r.Body != nil && r.ContentLength > 0 { defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return core.UserFacingError(core.INVALID_ARGUMENT, err.Error(), nil) + return core.NewPublicError(core.INVALID_ARGUMENT, err.Error(), nil) } } @@ -190,7 +190,7 @@ func parseBoolQueryParam(r *http.Request, name string) (bool, error) { var err error b, err = strconv.ParseBool(s) if err != nil { - return false, core.UserFacingError(core.INVALID_ARGUMENT, err.Error(), nil) + return false, core.NewPublicError(core.INVALID_ARGUMENT, err.Error(), nil) } } return b, nil diff --git a/go/plugins/firebase/auth.go b/go/plugins/firebase/auth.go index e3f6d7e270..bb1856f970 100644 --- a/go/plugins/firebase/auth.go +++ b/go/plugins/firebase/auth.go @@ -42,7 +42,7 @@ type AuthClient interface { func ContextProvider(ctx context.Context, g *genkit.Genkit, policy AuthPolicy) (core.ContextProvider, error) { f, ok := genkit.LookupPlugin(g, provider).(*Firebase) if !ok { - return nil, core.NewGenkitError(core.NOT_FOUND, "firebase plugin not initialized; did you pass the plugin to genkit.Init()") + return nil, core.NewError(core.NOT_FOUND, "firebase plugin not initialized; did you pass the plugin to genkit.Init()") } client, err := f.App.Auth(ctx) if err != nil { @@ -52,19 +52,19 @@ func ContextProvider(ctx context.Context, g *genkit.Genkit, policy AuthPolicy) ( return func(ctx context.Context, input core.RequestData) (core.ActionContext, error) { authHeader, ok := input.Headers["authorization"] if !ok { - return nil, core.UserFacingError(core.UNAUTHENTICATED, "authorization header is required but not provided", nil) + return nil, core.NewPublicError(core.UNAUTHENTICATED, "authorization header is required but not provided", nil) } const bearerPrefix = "bearer " if !strings.HasPrefix(strings.ToLower(authHeader), bearerPrefix) { - return nil, core.UserFacingError(core.UNAUTHENTICATED, "invalid authorization header format", nil) + return nil, core.NewPublicError(core.UNAUTHENTICATED, "invalid authorization header format", nil) } token := authHeader[len(bearerPrefix):] authCtx, err := client.VerifyIDToken(ctx, token) if err != nil { - return nil, core.UserFacingError(core.UNAUTHENTICATED, fmt.Sprintf("error verifying ID token: %v", err), nil) + return nil, core.NewPublicError(core.UNAUTHENTICATED, fmt.Sprintf("error verifying ID token: %v", err), nil) } if policy != nil { diff --git a/go/plugins/mcp/mcp.go b/go/plugins/mcp/mcp.go new file mode 100644 index 0000000000..a449ec33f5 --- /dev/null +++ b/go/plugins/mcp/mcp.go @@ -0,0 +1,3 @@ +package mcp + +// Main plugin definitions for MCP client and server diff --git a/go/samples/basic-gemini/main.go b/go/samples/basic-gemini/main.go index c9ff720529..f84368f16f 100644 --- a/go/samples/basic-gemini/main.go +++ b/go/samples/basic-gemini/main.go @@ -40,7 +40,7 @@ func main() { genkit.DefineFlow(g, "jokesFlow", func(ctx context.Context, input string) (string, error) { m := googlegenai.GoogleAIModel(g, "gemini-2.5-pro-preview-03-25") if m == nil { - return "", core.NewGenkitError(core.INVALID_ARGUMENT, "jokesFlow: failed to find model") + return "", core.NewError(core.INVALID_ARGUMENT, "jokesFlow: failed to find model") } resp, err := genkit.Generate(ctx, g, From ba33b412d720c146e90513709e994f95c42eb37c Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Wed, 16 Apr 2025 13:56:55 -0700 Subject: [PATCH 18/24] Delete mcp.go --- go/plugins/mcp/mcp.go | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 go/plugins/mcp/mcp.go diff --git a/go/plugins/mcp/mcp.go b/go/plugins/mcp/mcp.go deleted file mode 100644 index a449ec33f5..0000000000 --- a/go/plugins/mcp/mcp.go +++ /dev/null @@ -1,3 +0,0 @@ -package mcp - -// Main plugin definitions for MCP client and server From 419e63b43bd4e70c9cf41a4a9bf222ce99f2ad5c Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Thu, 17 Apr 2025 12:11:53 +0530 Subject: [PATCH 19/24] Define Userfacing error type --- go/core/error.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index 9ec88e52c0..d99e477888 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -43,18 +43,30 @@ type GenkitError struct { Source *string `json:"source,omitempty"` // Pointer for optional } +// UserFacingError is the base error type for user facing errors. +type UserFacingError struct { + Message string `json:"message"` // Exclude from default JSON if embedded elsewhere + Status StatusName `json:"status"` + Details map[string]any `json:"details"` // Use map for arbitrary details +} + // NewPublicError allows a web framework handler to know it // is safe to return the message in a request. Other kinds of errors will // result in a generic 500 message to avoid the possibility of internal // exceptions being leaked to attackers. -func NewPublicError(status StatusName, message string, details map[string]any) *GenkitError { - return &GenkitError{ +func NewPublicError(status StatusName, message string, details map[string]any) *UserFacingError { + return &UserFacingError{ Status: status, Details: details, Message: message, } } +// Error implements the standard error interface. +func (e *UserFacingError) Error() string { + return fmt.Sprintf("%s: %s", e.Status, e.Message) +} + // NewError creates a new GenkitError with a stack trace. func NewError(status StatusName, message string, args ...any) *GenkitError { // Prevents a compile-time warning about non-constant message. From fa67c715dd99f2c8c40de4c7e1b872a3bea14227 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Thu, 17 Apr 2025 12:12:46 +0530 Subject: [PATCH 20/24] Define Userfacing error type --- go/core/error.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/core/error.go b/go/core/error.go index d99e477888..00e118c533 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -62,7 +62,7 @@ func NewPublicError(status StatusName, message string, details map[string]any) * } } -// Error implements the standard error interface. +// Error implements the standard error interface for UserFacingError. func (e *UserFacingError) Error() string { return fmt.Sprintf("%s: %s", e.Status, e.Message) } From 6e205c61524f24c104155fb390f1469373e9cf26 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Thu, 17 Apr 2025 17:44:53 +0530 Subject: [PATCH 21/24] fix test case because of UserFacingError handling --- go/genkit/servers_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/genkit/servers_test.go b/go/genkit/servers_test.go index cd9cc60728..d203bbb99b 100644 --- a/go/genkit/servers_test.go +++ b/go/genkit/servers_test.go @@ -132,8 +132,8 @@ func TestHandler(t *testing.T) { resp := w.Result() body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != http.StatusBadRequest { - t.Errorf("want status code %d, got %d", http.StatusBadRequest, resp.StatusCode) + if resp.StatusCode != http.StatusInternalServerError { + t.Errorf("want status code %d, got %d", http.StatusInternalServerError, resp.StatusCode) } if !strings.Contains(string(body), "invalid character") { From 27496575793e5f57589083154f2b572b4c4bbabb Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Mon, 28 Apr 2025 19:37:27 +0530 Subject: [PATCH 22/24] generalise the error in genkit error --- go/core/error.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index 00e118c533..e9bfa0bb12 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -87,13 +87,10 @@ func NewError(status StatusName, message string, args ...any) *GenkitError { // Error implements the standard error interface. func (e *GenkitError) Error() string { - sourcePrefix := "" - if e.Source != nil && *e.Source != "" { - sourcePrefix = fmt.Sprintf("%s: ", *e.Source) + if e == nil { + return "" } - baseMsg := fmt.Sprintf("%s%s: %s", sourcePrefix, e.Status, e.Message) - - return baseMsg + return e.Message } // ToReflectionError returns a JSON-serializable representation for reflection API responses. From bfc194edf32ef4079408d60b3bcf0ef7a957ed06 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Tue, 29 Apr 2025 10:53:27 +0530 Subject: [PATCH 23/24] refactoring as per the comments --- go/core/error.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index e9bfa0bb12..d3b7bdcbb2 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -77,7 +77,7 @@ func NewError(status StatusName, message string, args ...any) *GenkitError { Message: fmt.Sprintf(msg, args...), } - errStack := getErrorStack(ge) + errStack := string(debug.Stack()) if errStack != "" { ge.Details = make(map[string]any) ge.Details["stack"] = errStack @@ -87,9 +87,6 @@ func NewError(status StatusName, message string, args ...any) *GenkitError { // Error implements the standard error interface. func (e *GenkitError) Error() string { - if e == nil { - return "" - } return e.Message } @@ -115,7 +112,7 @@ func ToReflectionError(err error) ReflectionError { return ge.ToReflectionError() } - stack := getErrorStack(err) + stack := string(debug.Stack()) detailsWire := &ReflectionErrorDetails{} if stack != "" { detailsWire.Stack = &stack @@ -127,12 +124,3 @@ func ToReflectionError(err error) ReflectionError { Details: detailsWire, } } - -// getErrorStack extracts stack trace from an error object. -// This captures the stack trace of the current goroutine when called. -func getErrorStack(err error) string { - if err == nil { - return "" - } - return string(debug.Stack()) -} From 2605fdc9f9cab7845b8c7d70ca39f5e7daba9487 Mon Sep 17 00:00:00 2001 From: Sahdev Garg Date: Wed, 30 Apr 2025 00:17:51 +0530 Subject: [PATCH 24/24] removal of debug stack --- go/core/error.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index d3b7bdcbb2..c51bed4102 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -112,15 +112,9 @@ func ToReflectionError(err error) ReflectionError { return ge.ToReflectionError() } - stack := string(debug.Stack()) - detailsWire := &ReflectionErrorDetails{} - if stack != "" { - detailsWire.Stack = &stack - } - return ReflectionError{ Message: err.Error(), Code: HTTPStatusCode(INTERNAL), - Details: detailsWire, + Details: &ReflectionErrorDetails{}, } }