Translate LLM backend capacity failures

This commit is contained in:
2026-07-30 02:23:51 +00:00
parent 71a004bfc8
commit 46e4466d28
7 changed files with 140 additions and 6 deletions

View File

@@ -5,3 +5,7 @@ import "errors"
// ErrInvalidStructuredOutput identifies a provider response that cannot satisfy
// the caller's declared structured-output contract.
var ErrInvalidStructuredOutput = errors.New("invalid structured output")
// ErrLLMCapacityExceeded identifies backend admission exhaustion before model
// generation begins.
var ErrLLMCapacityExceeded = errors.New("LLM capacity exceeded")

View File

@@ -3,6 +3,7 @@ package llm
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
@@ -131,6 +132,14 @@ func (c *PromptKitClient) CompleteStructured(ctx context.Context, req contracts.
if ctxErr := ctx.Err(); ctxErr != nil {
return contracts.StructuredCompletionResponse{}, ctxErr
}
if errors.Is(err, promptkit.ErrCapacityExceeded) {
return contracts.StructuredCompletionResponse{}, fmt.Errorf(
"run PromptKit prompt %q: %w: %v",
promptID,
contracts.ErrLLMCapacityExceeded,
redactPromptKitError(err),
)
}
return contracts.StructuredCompletionResponse{}, fmt.Errorf("run PromptKit prompt %q: %w", promptID, redactPromptKitError(err))
}
if result == nil {

View File

@@ -429,6 +429,89 @@ func TestPromptKitClientProviderFailureIncludesContextAndRedactsBearerToken(t *t
}
}
func TestPromptKitClientTranslatesBackendCapacityExhaustion(t *testing.T) {
queueCapacity := 0
fake := &fakePromptKitLLM{
err: errors.New("provider failed with Bearer secret-token"),
block: make(chan struct{}),
}
client, err := NewPromptKitClient(PromptKitClientConfig{
Assets: newTestPromptKitAssets(t),
EngineOptions: []promptkit.Option{
promptkit.WithBackend(promptkit.Backend{
ID: "limited-backend",
Endpoint: "http://127.0.0.1:1/v1",
ConcurrencyLimit: 1,
QueueCapacity: &queueCapacity,
}),
promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
ID: "limited-profile",
BackendID: "limited-backend",
Model: "limited-model",
})),
promptkit.WithLLMClient(fake),
},
})
if err != nil {
t.Fatalf("NewPromptKitClient() error = %v, want nil", err)
}
defer func() {
select {
case <-fake.block:
default:
close(fake.block)
}
}()
request := contracts.StructuredCompletionRequest{
PromptID: "adapter.direct-session",
ProfileID: "limited-profile",
Inputs: contracts.LLMInputSet{
"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
},
Vars: map[string]any{"custom": "value"},
}
firstResult := make(chan error, 1)
go func() {
var out map[string]any
_, callErr := client.CompleteStructured(context.Background(), request, &out)
firstResult <- callErr
}()
waitForAtomicAtLeast(t, &fake.calls, 1)
var out map[string]any
response, capacityErr := client.CompleteStructured(context.Background(), request, &out)
if len(response.Content) != 0 {
t.Fatalf("capacity response = %#v, want empty", response)
}
if !errors.Is(capacityErr, contracts.ErrLLMCapacityExceeded) {
t.Fatalf("capacity error = %v, want ErrLLMCapacityExceeded", capacityErr)
}
if errors.Is(capacityErr, contracts.ErrInvalidStructuredOutput) {
t.Fatalf("capacity error = %v, must not be invalid structured output", capacityErr)
}
if errors.Is(capacityErr, promptkit.ErrCapacityExceeded) {
t.Fatalf("capacity error exposes PromptKit sentinel: %v", capacityErr)
}
if !strings.Contains(capacityErr.Error(), `run PromptKit prompt "adapter.direct-session"`) ||
!strings.Contains(capacityErr.Error(), "backend capacity exceeded") {
t.Fatalf("capacity error = %q, want prompt context and upstream diagnostic", capacityErr)
}
if calls := atomic.LoadInt32(&fake.calls); calls != 1 {
t.Fatalf("provider calls after capacity rejection = %d, want 1", calls)
}
close(fake.block)
firstErr := <-firstResult
if firstErr == nil || strings.Contains(firstErr.Error(), "secret-token") ||
!strings.Contains(firstErr.Error(), "Bearer [REDACTED]") {
t.Fatalf("admitted provider error = %v, want redacted diagnostic", firstErr)
}
if calls := atomic.LoadInt32(&fake.calls); calls != 1 {
t.Fatalf("provider calls after release = %d, want no adapter retry", calls)
}
}
func TestPromptKitClientContextCancellationIsRespected(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()