Skip to content

Release Notes: v0.8.0

Release Date: 2026-04-03

Highlights

  • Resilience Package: New provider-agnostic error classification and retry logic
  • Smart Fallback: TTS and STT clients only switch providers on permanent errors
  • 8 Error Categories: Actionable error handling with clear retry decisions

New Features

Resilience Package

The new resilience package provides provider-agnostic error handling:

import "github.com/plexusone/omnivoice-core/resilience"

Error Categories

8 categories for actionable error handling:

Category Description Retryable
transient Temporary failures (timeout, network) Yes
rate_limit Rate limiting (429) Yes
server Server errors (500, 502, 503) Yes
validation Invalid request (400, 422) No
auth Authentication/authorization (401, 403) No
not_found Resource not found (404) No
quota Quota exceeded No
unknown Unclassified errors No

ProviderError Type

Wrap provider errors with classification metadata:

err := resilience.NewProviderError("elevenlabs", "Synthesize", originalErr, resilience.ErrorInfo{
    Category:   resilience.CategoryRateLimit,
    Retryable:  true,
    Code:       "RATE_LIMITED",
    Message:    "Rate limit exceeded",
    Suggestion: "Wait and retry the request",
    RetryAfter: 60 * time.Second,
})

// Check error type
if pe, ok := resilience.IsProviderError(err); ok {
    if pe.IsRetryable() {
        // Retry logic
    }
}

Retry with Configurable Backoff

Generic retry functions with backoff:

// Simple retry
err := resilience.Retry(ctx, config, func() error {
    return doSomething()
})

// Retry with result
result, err := resilience.RetryWithResult(ctx, config, func() (*Result, error) {
    return fetchData()
})

Configure retry behavior:

config := resilience.RetryConfig{
    MaxAttempts: 5,
    Backoff: &resilience.ExponentialBackoff{
        Initial:    1 * time.Second,
        Max:        30 * time.Second,
        Multiplier: 2.0,
        Jitter:     0.1,
    },
    Classifier: myClassifier,
    OnRetry: func(attempt int, err error, delay time.Duration) {
        log.Printf("Retry %d after %v", attempt, delay)
    },
}

Backoff Strategies

Four backoff strategies included:

// Exponential backoff with jitter (recommended)
backoff := &resilience.ExponentialBackoff{
    Initial:    1 * time.Second,
    Max:        30 * time.Second,
    Multiplier: 2.0,
    Jitter:     0.1, // 10% random jitter
}

// Linear backoff
backoff := &resilience.LinearBackoff{
    Initial:   1 * time.Second,
    Increment: 1 * time.Second,
    Max:       10 * time.Second,
}

// Constant delay
backoff := &resilience.ConstantBackoff{
    Delay: 1 * time.Second,
}

// No delay (for testing)
backoff := &resilience.NoBackoff{}

Error Classification Interface

Implement custom classifiers:

type ErrorClassifier interface {
    Classify(err error) ErrorInfo
}

// Built-in HTTP classifier
classifier := resilience.NewHTTPStatusClassifier()
info := classifier.Classify(httpError)

Smart Fallback in TTS/STT Clients

TTS and STT clients now use smart fallback logic:

import "github.com/plexusone/omnivoice-core/tts"

client := tts.NewClient(
    tts.WithProvider("elevenlabs", elevenLabsProvider),
    tts.WithProvider("deepgram", deepgramProvider),
    tts.WithFallbacks("deepgram"),
)

// Smart fallback behavior:
// - Rate limit (429) → provider retries internally → succeed or fail
// - Auth error (401) → no retry → fallback to Deepgram
// - Server error (500) → provider retries internally → fallback if exhausted
result, err := client.Synthesize(ctx, "Hello world", config)

Fallback decision logic:

Error Type Provider Action Client Action
Retryable (429, 500) Retry with backoff Wait for provider
Permanent (401, 404) Return immediately Fallback
Unknown Return immediately Fallback

Use Cases

Building Resilient Voice Applications

func synthesizeSpeech(ctx context.Context, text string) ([]byte, error) {
    // Provider handles retries internally
    result, err := provider.Synthesize(ctx, text, config)
    if err != nil {
        if pe, ok := resilience.IsProviderError(err); ok {
            switch pe.GetCategory() {
            case resilience.CategoryAuth:
                return nil, fmt.Errorf("authentication failed: %s", pe.Info.Suggestion)
            case resilience.CategoryValidation:
                return nil, fmt.Errorf("invalid request: %s", pe.Info.Message)
            case resilience.CategoryRateLimit:
                // Already retried, rate limit persists
                return nil, fmt.Errorf("rate limited after retries: %w", err)
            }
        }
        return nil, err
    }
    return result.Audio, nil
}

Custom Error Classification

type MyClassifier struct{}

func (c *MyClassifier) Classify(err error) resilience.ErrorInfo {
    // Custom classification logic
    if strings.Contains(err.Error(), "timeout") {
        return resilience.ErrorInfo{
            Category:  resilience.CategoryTransient,
            Retryable: true,
            Message:   "Request timed out",
            Suggestion: "Check network connectivity",
        }
    }
    return resilience.ErrorInfo{
        Category:  resilience.CategoryUnknown,
        Retryable: false,
    }
}

Testing Retry Logic

func TestRetryOnRateLimit(t *testing.T) {
    attempts := 0
    err := resilience.Retry(ctx, resilience.RetryConfig{
        MaxAttempts: 3,
        Backoff:     &resilience.NoBackoff{}, // Fast for testing
        Classifier:  myClassifier,
    }, func() error {
        attempts++
        if attempts < 3 {
            return rateLimitError
        }
        return nil
    })

    if err != nil {
        t.Errorf("Expected success after retries, got: %v", err)
    }
    if attempts != 3 {
        t.Errorf("Expected 3 attempts, got %d", attempts)
    }
}

API Reference

Types

Type Description
ErrorCategory String enum for error categories
ErrorInfo Metadata about an error (category, retryable, code, message, suggestion, retry-after)
ProviderError Error wrapper with provider name, operation, and ErrorInfo
RetryConfig Configuration for retry behavior
RetryError Error returned when retries are exhausted
Backoff Interface for backoff strategies

Functions

Function Description
NewProviderError(provider, op, err, info) Create a ProviderError
IsProviderError(err) Extract ProviderError from error chain
IsRetryable(err) Check if error is retryable
Retry(ctx, config, fn) Execute with retries
RetryWithResult[T](ctx, config, fn) Execute with retries, return result
DefaultBackoff() Create default exponential backoff
DefaultRetryConfig() Create default retry configuration
NewHTTPStatusClassifier() Create HTTP status code classifier

Constants

const (
    CategoryTransient  ErrorCategory = "transient"
    CategoryRateLimit  ErrorCategory = "rate_limit"
    CategoryValidation ErrorCategory = "validation"
    CategoryAuth       ErrorCategory = "auth"
    CategoryNotFound   ErrorCategory = "not_found"
    CategoryServer     ErrorCategory = "server"
    CategoryQuota      ErrorCategory = "quota"
    CategoryUnknown    ErrorCategory = "unknown"
)

Installation

go get github.com/plexusone/omnivoice-core@v0.8.0

Migration Guide

From v0.7.0

No breaking changes to existing APIs. To use the new features:

  1. Update dependency:
go get github.com/plexusone/omnivoice-core@v0.8.0
  1. (Optional) Implement ErrorClassifier in your providers to enable smart fallback

  2. TTS/STT clients automatically use smart fallback when providers return ProviderError

Full Changelog

See CHANGELOG.md for the complete list of changes.