ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

woof fix usage logging till we get https://github.com/openclosed-dev/slogan/pull/3 in (#488)

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
6fbdea5f d68404e4

+153 -26
+33 -2
internal/ai/client.go
··· 184 184 ) 185 185 186 186 func responseToShoppingList(ctx context.Context, resp *responses.Response) (*ShoppingList, error) { 187 - slog.InfoContext(ctx, "API usage", slog.Any("usage", json.RawMessage(resp.Usage.RawJSON()))) 187 + slog.InfoContext(ctx, "API usage", responseUsageLogAttr(resp.Usage)) 188 188 var shoppingList ShoppingList 189 189 if err := json.Unmarshal([]byte(resp.OutputText()), &shoppingList); err != nil { 190 190 return nil, fmt.Errorf("failed to parse AI response: %w", err) ··· 291 291 return nil, fmt.Errorf("failed to generate recipe image: %w", err) 292 292 } 293 293 294 - slog.InfoContext(ctx, "API usage", slog.Any("usage", json.RawMessage(resp.Usage.RawJSON()))) 294 + slog.InfoContext(ctx, "API usage", imageUsageLogAttr(resp.Usage)) 295 295 if len(resp.Data) == 0 { 296 296 return nil, fmt.Errorf("image generation returned no images") 297 297 } ··· 303 303 return &GeneratedImage{ 304 304 Body: base64.NewDecoder(base64.StdEncoding, strings.NewReader(imageBody)), 305 305 }, nil 306 + } 307 + 308 + // remove this and imageUsage if we get https://github.com/openclosed-dev/slogan/pull/3 in 309 + func responseUsageLogAttr(usage responses.ResponseUsage) slog.Attr { 310 + return slog.Group("usage", 311 + slog.Int64("inputTokens", usage.InputTokens), 312 + slog.Group("inputTokensDetails", 313 + slog.Int64("cachedTokens", usage.InputTokensDetails.CachedTokens), 314 + ), 315 + slog.Int64("outputTokens", usage.OutputTokens), 316 + slog.Group("outputTokensDetails", 317 + slog.Int64("reasoningTokens", usage.OutputTokensDetails.ReasoningTokens), 318 + ), 319 + slog.Int64("totalTokens", usage.TotalTokens), 320 + ) 321 + } 322 + 323 + func imageUsageLogAttr(usage openai.ImagesResponseUsage) slog.Attr { 324 + return slog.Group("usage", 325 + slog.Int64("inputTokens", usage.InputTokens), 326 + slog.Group("inputTokensDetails", 327 + slog.Int64("imageTokens", usage.InputTokensDetails.ImageTokens), 328 + slog.Int64("textTokens", usage.InputTokensDetails.TextTokens), 329 + ), 330 + slog.Int64("outputTokens", usage.OutputTokens), 331 + slog.Group("outputTokensDetails", 332 + slog.Int64("imageTokens", usage.OutputTokensDetails.ImageTokens), 333 + slog.Int64("textTokens", usage.OutputTokensDetails.TextTokens), 334 + ), 335 + slog.Int64("totalTokens", usage.TotalTokens), 336 + ) 306 337 } 307 338 308 339 func (c *Client) PickWine(ctx context.Context, recipe Recipe, wines []kroger.Ingredient) (*WineSelection, error) {
+18 -7
internal/ai/critique.go
··· 115 115 "model_version", resp.ModelVersion, 116 116 "response_id", resp.ResponseID, 117 117 "latencyMS", time.Since(start).Milliseconds(), 118 - "usage", geminiUsageLogValue(resp.UsageMetadata), 118 + geminiUsageLogAttr(resp.UsageMetadata), 119 119 ) 120 120 121 121 critique, err := parseRecipeCritique(resp.Text()) ··· 127 127 return critique, nil 128 128 } 129 129 130 - func geminiUsageLogValue(usage *genai.GenerateContentResponseUsageMetadata) any { 130 + // remove if we get https://github.com/openclosed-dev/slogan/pull/3 in 131 + func geminiUsageLogAttr(usage *genai.GenerateContentResponseUsageMetadata) slog.Attr { 131 132 if usage == nil { 132 - return json.RawMessage("null") 133 + return slog.Group("usage", slog.Bool("available", false)) 134 + } 135 + 136 + attrs := []any{ 137 + slog.Bool("available", true), 138 + slog.Int("cachedContentTokenCount", int(usage.CachedContentTokenCount)), 139 + slog.Int("promptTokenCount", int(usage.PromptTokenCount)), 140 + slog.Int("candidatesTokenCount", int(usage.CandidatesTokenCount)), 141 + slog.Int("thoughtsTokenCount", int(usage.ThoughtsTokenCount)), 142 + slog.Int("toolUsePromptTokenCount", int(usage.ToolUsePromptTokenCount)), 143 + slog.Int("totalTokenCount", int(usage.TotalTokenCount)), 133 144 } 134 - body, err := json.Marshal(usage) 135 - if err != nil { 136 - return fmt.Sprintf("failed to marshal Gemini usage metadata: %v", err) 145 + if usage.TrafficType != "" { 146 + attrs = append(attrs, slog.String("trafficType", string(usage.TrafficType))) 137 147 } 138 - return json.RawMessage(body) 148 + 149 + return slog.Group("usage", attrs...) 139 150 } 140 151 141 152 func (c *critiquer) newClient(ctx context.Context) (*genai.Client, error) {
+29 -17
internal/ai/critique_test.go
··· 1 1 package ai 2 2 3 3 import ( 4 - "encoding/json" 4 + "log/slog" 5 5 "testing" 6 6 7 7 "github.com/stretchr/testify/assert" ··· 92 92 assert.Equal(t, float64(10), overallScore["maximum"]) 93 93 } 94 94 95 - func TestGeminiUsageLogValue(t *testing.T) { 95 + func TestGeminiUsageLogAttr(t *testing.T) { 96 96 t.Run("nil usage", func(t *testing.T) { 97 - raw, ok := geminiUsageLogValue(nil).(json.RawMessage) 98 - require.True(t, ok) 99 - assert.JSONEq(t, `null`, string(raw)) 97 + attr := geminiUsageLogAttr(nil) 98 + assert.Equal(t, "usage", attr.Key) 99 + assert.Equal(t, slog.KindGroup, attr.Value.Kind()) 100 + require.Len(t, attr.Value.Group(), 1) 101 + assert.Equal(t, slog.Bool("available", false), attr.Value.Group()[0]) 100 102 }) 101 103 102 - t.Run("usage marshals to json", func(t *testing.T) { 103 - raw, ok := geminiUsageLogValue(&genai.GenerateContentResponseUsageMetadata{ 104 - PromptTokenCount: 448, 105 - CandidatesTokenCount: 986, 106 - TotalTokenCount: 1877, 107 - }).(json.RawMessage) 108 - require.True(t, ok) 109 - assert.JSONEq(t, `{ 110 - "promptTokenCount": 448, 111 - "candidatesTokenCount": 986, 112 - "totalTokenCount": 1877 113 - }`, string(raw)) 104 + t.Run("usage becomes a slog group", func(t *testing.T) { 105 + attr := geminiUsageLogAttr(&genai.GenerateContentResponseUsageMetadata{ 106 + CachedContentTokenCount: 22, 107 + PromptTokenCount: 448, 108 + CandidatesTokenCount: 986, 109 + ThoughtsTokenCount: 111, 110 + ToolUsePromptTokenCount: 310, 111 + TotalTokenCount: 1877, 112 + TrafficType: genai.TrafficTypeOnDemand, 113 + }) 114 + assert.Equal(t, "usage", attr.Key) 115 + assert.Equal(t, slog.KindGroup, attr.Value.Kind()) 116 + assert.Equal(t, []slog.Attr{ 117 + slog.Bool("available", true), 118 + slog.Int("cachedContentTokenCount", 22), 119 + slog.Int("promptTokenCount", 448), 120 + slog.Int("candidatesTokenCount", 986), 121 + slog.Int("thoughtsTokenCount", 111), 122 + slog.Int("toolUsePromptTokenCount", 310), 123 + slog.Int("totalTokenCount", 1877), 124 + slog.String("trafficType", string(genai.TrafficTypeOnDemand)), 125 + }, attr.Value.Group()) 114 126 }) 115 127 }
+73
internal/ai/recipe_test.go
··· 1 1 package ai 2 2 3 3 import ( 4 + "log/slog" 5 + "reflect" 4 6 "slices" 5 7 "strings" 6 8 "testing" 7 9 8 10 "careme/internal/kroger" 11 + 12 + openai "github.com/openai/openai-go/v3" 13 + "github.com/openai/openai-go/v3/responses" 9 14 ) 10 15 11 16 func TestRecipeComputeHash(t *testing.T) { ··· 151 156 } 152 157 if !strings.Contains(prompt, "Candidate wines TSV:\nProductId\tAisleNumber\tBrand\tDescription\tSize\tPriceRegular\tPriceSale\n\t\t\tPinot Noir\t750mL\t13.99\t13.99\n") { 153 158 t.Fatalf("expected candidate wines TSV in prompt: %s", prompt) 159 + } 160 + } 161 + 162 + func TestResponseUsageLogAttr(t *testing.T) { 163 + attr := responseUsageLogAttr(responses.ResponseUsage{ 164 + InputTokens: 1200, 165 + OutputTokens: 350, 166 + TotalTokens: 1550, 167 + InputTokensDetails: responses.ResponseUsageInputTokensDetails{ 168 + CachedTokens: 900, 169 + }, 170 + OutputTokensDetails: responses.ResponseUsageOutputTokensDetails{ 171 + ReasoningTokens: 125, 172 + }, 173 + }) 174 + 175 + if attr.Key != "usage" { 176 + t.Fatalf("unexpected attr key: %s", attr.Key) 177 + } 178 + if attr.Value.Kind() != slog.KindGroup { 179 + t.Fatalf("unexpected attr kind: %v", attr.Value.Kind()) 180 + } 181 + if !reflect.DeepEqual(attr.Value.Group(), []slog.Attr{ 182 + slog.Int64("inputTokens", 1200), 183 + slog.Group("inputTokensDetails", slog.Int64("cachedTokens", 900)), 184 + slog.Int64("outputTokens", 350), 185 + slog.Group("outputTokensDetails", slog.Int64("reasoningTokens", 125)), 186 + slog.Int64("totalTokens", 1550), 187 + }) { 188 + t.Fatalf("unexpected attrs: %#v", attr.Value.Group()) 189 + } 190 + } 191 + 192 + func TestImageUsageLogAttr(t *testing.T) { 193 + attr := imageUsageLogAttr(openai.ImagesResponseUsage{ 194 + InputTokens: 100, 195 + OutputTokens: 200, 196 + TotalTokens: 300, 197 + InputTokensDetails: openai.ImagesResponseUsageInputTokensDetails{ 198 + ImageTokens: 60, 199 + TextTokens: 40, 200 + }, 201 + OutputTokensDetails: openai.ImagesResponseUsageOutputTokensDetails{ 202 + ImageTokens: 180, 203 + TextTokens: 20, 204 + }, 205 + }) 206 + 207 + if attr.Key != "usage" { 208 + t.Fatalf("unexpected attr key: %s", attr.Key) 209 + } 210 + if attr.Value.Kind() != slog.KindGroup { 211 + t.Fatalf("unexpected attr kind: %v", attr.Value.Kind()) 212 + } 213 + if !reflect.DeepEqual(attr.Value.Group(), []slog.Attr{ 214 + slog.Int64("inputTokens", 100), 215 + slog.Group("inputTokensDetails", 216 + slog.Int64("imageTokens", 60), 217 + slog.Int64("textTokens", 40), 218 + ), 219 + slog.Int64("outputTokens", 200), 220 + slog.Group("outputTokensDetails", 221 + slog.Int64("imageTokens", 180), 222 + slog.Int64("textTokens", 20), 223 + ), 224 + slog.Int64("totalTokens", 300), 225 + }) { 226 + t.Fatalf("unexpected attrs: %#v", attr.Value.Group()) 154 227 } 155 228 } 156 229