summaryrefslogtreecommitdiff
path: root/internal/image
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-08 10:14:40 +0300
committerPaul Buetow <paul@buetow.org>2026-04-08 10:14:40 +0300
commitf7deedf50f6fa00c7dfe989ed381989e202b3057 (patch)
tree72fe642a99101da79d2103af911c8dcf88b08484 /internal/image
parent14995ca7a76e2a72a2169def7f55f3b9e2433ee2 (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.go33
-rw-r--r--internal/image/openai.go11
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)
}