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
|
||||
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
|
||||
|
||||
**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 {
|
||||
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
|
||||
if errors.As(err, &internalCapacityError) && internalCapacityError != nil &&
|
||||
strings.TrimSpace(internalCapacityError.BackendID) != "" {
|
||||
|
||||
@@ -80,3 +80,12 @@ func TestMapPublicErrorDoesNotTreatRequestEncodingAsTransport(t *testing.T) {
|
||||
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