Translate LLM backend capacity failures
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user