Preserve owning categories in public errors
This commit is contained in:
@@ -147,6 +147,11 @@ for their respective later units.
|
|||||||
preserve `errors.Is`/`errors.As`, existing capacity metadata, exhaustion and
|
preserve `errors.Is`/`errors.As`, existing capacity metadata, exhaustion and
|
||||||
provider projections, and cancellation identity.
|
provider projections, and cancellation identity.
|
||||||
|
|
||||||
|
Completion: public projection preserves the owning operation alongside nested
|
||||||
|
causes and specialized projections. Root and generation tests pass on Go 1.27.3;
|
||||||
|
two use-case preparation-classification tests await the direct preparation
|
||||||
|
marker specified below.
|
||||||
|
|
||||||
## Stage 5: Retain Successful-Response Read Failures
|
## Stage 5: Retain Successful-Response Read Failures
|
||||||
|
|
||||||
**Catalog:** 8. **Files:** `internal/llm/openai_compatible_client.go` and focused
|
**Catalog:** 8. **Files:** `internal/llm/openai_compatible_client.go` and focused
|
||||||
|
|||||||
25
errors.go
25
errors.go
@@ -17,6 +17,31 @@ func mapPublicError(err error) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
var operation *usecase.OperationError
|
||||||
|
if errors.As(err, &operation) && operation != nil {
|
||||||
|
owner := publicErrorFor(operation.Category)
|
||||||
|
if owner != nil {
|
||||||
|
// Project the underlying specialized error separately, retaining both
|
||||||
|
// the outer operation and the original cause for Is/As callers.
|
||||||
|
mapped := mapPublicError(operation.Cause)
|
||||||
|
if errors.Is(operation.Cause, llm.ErrTransport) {
|
||||||
|
mapped = errors.Join(ErrTransport, mapped)
|
||||||
|
}
|
||||||
|
if errors.Is(operation.Cause, llm.ErrMalformedResponse) {
|
||||||
|
mapped = errors.Join(ErrMalformedResponse, mapped)
|
||||||
|
}
|
||||||
|
var internalCapacity *usecase.CapacityError
|
||||||
|
var internalProvider *llm.ProviderHTTPError
|
||||||
|
if errors.As(operation.Cause, &internalCapacity) || errors.As(operation.Cause, &internalProvider) {
|
||||||
|
return errors.Join(owner, mapped)
|
||||||
|
}
|
||||||
|
return errors.Join(owner, mapped, operation.Cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mapUnownedPublicError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapUnownedPublicError(err error) error {
|
||||||
var internalCapacityError *usecase.CapacityError
|
var internalCapacityError *usecase.CapacityError
|
||||||
if errors.As(err, &internalCapacityError) && internalCapacityError != nil &&
|
if errors.As(err, &internalCapacityError) && internalCapacityError != nil &&
|
||||||
strings.TrimSpace(internalCapacityError.BackendID) != "" {
|
strings.TrimSpace(internalCapacityError.BackendID) != "" {
|
||||||
|
|||||||
@@ -80,3 +80,12 @@ func TestMapPublicErrorDoesNotTreatRequestEncodingAsTransport(t *testing.T) {
|
|||||||
t.Fatalf("request encoding category = %v", err)
|
t.Fatalf("request encoding category = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMapPublicErrorRetainsOwnerWithSpecializedCapacity(t *testing.T) {
|
||||||
|
internal := &usecase.CapacityError{BackendID: "limited"}
|
||||||
|
err := mapPublicError(&usecase.OperationError{Category: usecase.ErrLLMGenerate, Cause: internal})
|
||||||
|
var capacity *CapacityError
|
||||||
|
if !errors.Is(err, ErrLLMGenerate) || !errors.As(err, &capacity) || capacity.BackendID != "limited" || !errors.Is(err, ErrCapacityExceeded) {
|
||||||
|
t.Fatalf("mapped owned capacity = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
35
nested_error_contract_test.go
Normal file
35
nested_error_contract_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nestedErrorReader struct{ cause error }
|
||||||
|
|
||||||
|
func (r nestedErrorReader) Read(context.Context, promptkit.ArtifactRef) (*promptkit.Artifact, error) {
|
||||||
|
return nil, r.cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNestedEngineErrorKeepsOwningOperation(t *testing.T) {
|
||||||
|
child := newSimpleEngine(t, "child")
|
||||||
|
_, childErr := child.Run(context.Background(), promptkit.RunRequest{PromptID: "missing"})
|
||||||
|
if !errors.Is(childErr, promptkit.ErrPromptNotFound) {
|
||||||
|
t.Fatalf("child error = %v", childErr)
|
||||||
|
}
|
||||||
|
client := &fakeLLMClient{err: childErr}
|
||||||
|
parent := newSimpleEngine(t, "hello", promptkit.WithLLMClient(client))
|
||||||
|
_, err := parent.Run(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
||||||
|
if !errors.Is(err, promptkit.ErrLLMGenerate) || !errors.Is(err, childErr) || !errors.Is(err, promptkit.ErrPromptNotFound) {
|
||||||
|
t.Fatalf("generation error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
readerParent := newSimpleEngine(t, "hello", promptkit.WithArtifactReader(nestedErrorReader{cause: childErr}))
|
||||||
|
_, err = readerParent.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt", Inputs: map[string]promptkit.ArtifactRef{"extra": promptkit.Inline("data")}})
|
||||||
|
if !errors.Is(err, promptkit.ErrArtifactLoad) || !errors.Is(err, childErr) || !errors.Is(err, promptkit.ErrPromptNotFound) {
|
||||||
|
t.Fatalf("artifact error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user