diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-08 10:14:40 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-08 10:14:40 +0300 |
| commit | f7deedf50f6fa00c7dfe989ed381989e202b3057 (patch) | |
| tree | 72fe642a99101da79d2103af911c8dcf88b08484 /internal/image | |
| parent | 14995ca7a76e2a72a2169def7f55f3b9e2433ee2 (diff) | |
feat: add gobreaker circuit breakers for OpenAI and Gemini API calls
Introduce internal/apicircuit with sony/gobreaker: trip after five
consecutive failures, 45s open state, 2m count reset, half-open trial cap.
context.Canceled is not counted as a failure for breaker stats.
Wrap OpenAI TTS, Gemini TTS, OpenAI DALL-E/chat (image path), and Nano Banana
Gemini GenerateContent calls. HTTP timeouts remain unchanged in httpctx.
Made-with: Cursor
Diffstat (limited to 'internal/image')
| -rw-r--r-- | internal/image/nanobanana.go | 33 | ||||
| -rw-r--r-- | internal/image/openai.go | 11 |
2 files changed, 28 insertions, 16 deletions
diff --git a/internal/image/nanobanana.go b/internal/image/nanobanana.go index 7694069..4d12d08 100644 --- a/internal/image/nanobanana.go +++ b/internal/image/nanobanana.go @@ -17,6 +17,7 @@ import ( "google.golang.org/genai" + "codeberg.org/snonux/totalrecall/internal/apicircuit" "codeberg.org/snonux/totalrecall/internal/config" "codeberg.org/snonux/totalrecall/internal/httpctx" ) @@ -382,12 +383,14 @@ func (c *NanoBananaClient) generateSceneDescription(ctx context.Context, bulgari func (c *NanoBananaClient) generateText(ctx context.Context, model, systemPrompt, userPrompt string, temperature float32, maxOutputTokens int32) (string, error) { temp := temperature - resp, err := c.client.Models.GenerateContent(ctx, model, []*genai.Content{ - genai.NewContentFromText(userPrompt, genai.RoleUser), - }, &genai.GenerateContentConfig{ - SystemInstruction: genai.NewContentFromText(systemPrompt, genai.RoleUser), - Temperature: &temp, - MaxOutputTokens: maxOutputTokens, + resp, err := apicircuit.GeminiNanoBanana(func() (*genai.GenerateContentResponse, error) { + return c.client.Models.GenerateContent(ctx, model, []*genai.Content{ + genai.NewContentFromText(userPrompt, genai.RoleUser), + }, &genai.GenerateContentConfig{ + SystemInstruction: genai.NewContentFromText(systemPrompt, genai.RoleUser), + Temperature: &temp, + MaxOutputTokens: maxOutputTokens, + }) }) if err != nil { return "", fmt.Errorf("gemini API error: %w", err) @@ -412,9 +415,11 @@ func (c *NanoBananaClient) generateImage(ctx context.Context, prompt, aspectRati }, } - resp, err := c.client.Models.GenerateContent(ctx, c.modelName(), []*genai.Content{ - genai.NewContentFromText(prompt, genai.RoleUser), - }, cfg) + resp, err := apicircuit.GeminiNanoBanana(func() (*genai.GenerateContentResponse, error) { + return c.client.Models.GenerateContent(ctx, c.modelName(), []*genai.Content{ + genai.NewContentFromText(prompt, genai.RoleUser), + }, cfg) + }) if err != nil { return nil, "", &SearchError{ Provider: nanoBananaSource, @@ -466,10 +471,12 @@ func (c *NanoBananaClient) generateImageWithRefs(ctx context.Context, prompt, as ) parts = append(parts, &genai.Part{Text: refNote + prompt}) - resp, err := c.client.Models.GenerateContent(ctx, c.modelName(), - []*genai.Content{{Role: string(genai.RoleUser), Parts: parts}}, - cfg, - ) + resp, err := apicircuit.GeminiNanoBanana(func() (*genai.GenerateContentResponse, error) { + return c.client.Models.GenerateContent(ctx, c.modelName(), + []*genai.Content{{Role: string(genai.RoleUser), Parts: parts}}, + cfg, + ) + }) if err != nil { return nil, "", &SearchError{ Provider: nanoBananaSource, diff --git a/internal/image/openai.go b/internal/image/openai.go index 9fb8148..1bd5b2d 100644 --- a/internal/image/openai.go +++ b/internal/image/openai.go @@ -12,6 +12,7 @@ import ( "github.com/sashabaranov/go-openai" + "codeberg.org/snonux/totalrecall/internal/apicircuit" "codeberg.org/snonux/totalrecall/internal/httpctx" ) @@ -154,8 +155,10 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc req.Style = c.style } - // Generate the image - resp, err := c.client.CreateImage(ctx, req) + // Generate the image (circuit breaker limits load when OpenAI is unhealthy). + resp, err := apicircuit.OpenAIImage(func() (openai.ImageResponse, error) { + return c.client.CreateImage(ctx, req) + }) if err != nil { return nil, &SearchError{ Provider: "openai", @@ -295,7 +298,9 @@ func (c *OpenAIClient) generateSceneDescription(ctx context.Context, bulgarianWo MaxTokens: 100, } - resp, err := c.client.CreateChatCompletion(ctx, req) + resp, err := apicircuit.OpenAIImage(func() (openai.ChatCompletionResponse, error) { + return c.client.CreateChatCompletion(ctx, req) + }) if err != nil { return "", fmt.Errorf("scene generation failed: %w", err) } |
