From 37b18edf3df7f87bde4251e720d02dda002007db Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 9 Aug 2026 00:44:38 +0000 Subject: [PATCH] Contain provider errors at the LLM adapter --- internal/framework/llm/promptkit_client.go | 18 +++++------- .../framework/llm/promptkit_client_test.go | 29 ++++++++++++++++++- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/internal/framework/llm/promptkit_client.go b/internal/framework/llm/promptkit_client.go index 7c0dbe8..023b942 100644 --- a/internal/framework/llm/promptkit_client.go +++ b/internal/framework/llm/promptkit_client.go @@ -142,7 +142,7 @@ func (c *PromptKitClient) CompleteStructured(ctx context.Context, req contracts. if ctxErr := ctx.Err(); ctxErr != nil { return contracts.StructuredCompletionResponse{}, ctxErr } - return contracts.StructuredCompletionResponse{}, fmt.Errorf("prepare PromptKit prompt %q: %w", promptID, redactPromptKitError(err)) + return contracts.StructuredCompletionResponse{}, fmt.Errorf("prepare PromptKit prompt %q: %v", promptID, redactPromptKitError(err)) } defer prepared.Discard() preparedDetails := prepared.Details() @@ -169,7 +169,7 @@ func (c *PromptKitClient) CompleteStructured(ctx context.Context, req contracts. redactPromptKitError(err), ) } - return contracts.StructuredCompletionResponse{}, fmt.Errorf("run PromptKit prompt %q: %w", promptID, redactPromptKitError(err)) + return contracts.StructuredCompletionResponse{}, fmt.Errorf("run PromptKit prompt %q: %v", promptID, redactPromptKitError(err)) } if result == nil { return contracts.StructuredCompletionResponse{}, fmt.Errorf("run PromptKit prompt %q: %w: empty result", promptID, contracts.ErrInvalidStructuredOutput) @@ -427,17 +427,13 @@ func redactPromptKitError(err error) error { if err == nil { return nil } - return redactedProviderError{err: err} + return sanitizedProviderDiagnostic{message: bearerTokenPattern.ReplaceAllString(err.Error(), "Bearer "+secretReplacement)} } -type redactedProviderError struct { - err error +type sanitizedProviderDiagnostic struct { + message string } -func (e redactedProviderError) Error() string { - return bearerTokenPattern.ReplaceAllString(e.err.Error(), "Bearer "+secretReplacement) -} - -func (e redactedProviderError) Unwrap() error { - return e.err +func (e sanitizedProviderDiagnostic) Error() string { + return e.message } diff --git a/internal/framework/llm/promptkit_client_test.go b/internal/framework/llm/promptkit_client_test.go index 0465fe1..41ad4c7 100644 --- a/internal/framework/llm/promptkit_client_test.go +++ b/internal/framework/llm/promptkit_client_test.go @@ -741,6 +741,11 @@ model: local-model !strings.Contains(err.Error(), promptkit.BackendLocal) { t.Fatalf("CompleteStructured() without registration error = %v, want preparation failure with local backend context", err) } + for _, sentinel := range []error{promptkit.ErrProfileLoad, promptkit.ErrInvalidRequest, promptkit.ErrPromptNotFound} { + if errors.Is(err, sentinel) { + t.Fatalf("preparation failure exposes PromptKit sentinel %v: %v", sentinel, err) + } + } if providerCalls.Load() != 1 { t.Fatalf("provider calls after missing-registration failure = %d, want 1", providerCalls.Load()) } @@ -814,7 +819,8 @@ func TestPromptKitClientDecodeFailureReturnsRawResponse(t *testing.T) { } func TestPromptKitClientProviderFailureIncludesContextAndRedactsBearerToken(t *testing.T) { - client := newTestPromptKitClient(t, &fakePromptKitLLM{err: errors.New("provider failed with Bearer secret-token")}) + providerErr := &credentialBearingProviderError{} + client := newTestPromptKitClient(t, &fakePromptKitLLM{err: providerErr}) var out map[string]any resp, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{ @@ -836,6 +842,21 @@ func TestPromptKitClientProviderFailureIncludesContextAndRedactsBearerToken(t *t if strings.Contains(err.Error(), "secret-token") || !strings.Contains(err.Error(), "Bearer [REDACTED]") { t.Fatalf("error = %q, want redacted bearer token", err.Error()) } + for current := err; current != nil; current = errors.Unwrap(current) { + if strings.Contains(current.Error(), "secret-token") { + t.Fatalf("error chain exposes credential: %v", err) + } + } + if errors.Unwrap(err) != nil { + t.Fatalf("provider failure must not expose a wrapped diagnostic: %v", err) + } + var recoveredProviderErr *credentialBearingProviderError + if errors.As(err, &recoveredProviderErr) { + t.Fatalf("provider error escaped the adapter: %v", err) + } + if errors.Is(err, promptkit.ErrLLMGenerate) { + t.Fatalf("provider failure exposes PromptKit generation sentinel: %v", err) + } if resp.Debug != nil { t.Fatalf("debug material = %#v, want none for provider failure without result", resp.Debug) } @@ -1214,6 +1235,12 @@ type fakePromptKitLLM struct { maxInFlight int32 } +type credentialBearingProviderError struct{} + +func (*credentialBearingProviderError) Error() string { + return "provider failed with Bearer secret-token" +} + func (f *fakePromptKitLLM) Generate(ctx context.Context, req promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) { f.mu.Lock() f.last = req