77 "errors"
88 "fmt"
99 "strings"
10+ "time"
1011
1112 "github.com/MakeNowJust/heredoc"
1213 "github.com/github/gh-models/internal/azuremodels"
@@ -80,6 +81,8 @@ func NewEvalCommand(cfg *command.Config) *cobra.Command {
8081
8182 By default, results are displayed in a human-readable format. Use the --json flag
8283 to output structured JSON data for programmatic use or integration with CI/CD pipelines.
84+ This command will automatically retry on rate limiting errors, waiting for the specified
85+ duration before retrying the request.
8386
8487 See https://docs.github.com/github-models/use-github-models/storing-prompts-in-github-repositories#supported-file-format for more information.
8588 ` ),
@@ -327,36 +330,65 @@ func (h *evalCommandHandler) templateString(templateStr string, data map[string]
327330 return prompt .TemplateString (templateStr , data )
328331}
329332
330- func (h * evalCommandHandler ) callModel (ctx context.Context , messages []azuremodels.ChatMessage ) (string , error ) {
331- req := h .evalFile .BuildChatCompletionOptions (messages )
332-
333- resp , err := h .client .GetChatCompletionStream (ctx , req , h .org )
334- if err != nil {
335- return "" , err
336- }
333+ // callModelWithRetry makes an API call with automatic retry on rate limiting
334+ func (h * evalCommandHandler ) callModelWithRetry (ctx context.Context , req azuremodels.ChatCompletionOptions ) (string , error ) {
335+ const maxRetries = 3
337336
338- // For non-streaming requests, we should get a single response
339- var content strings.Builder
340- for {
341- completion , err := resp .Reader .Read ()
337+ for attempt := 0 ; attempt <= maxRetries ; attempt ++ {
338+ resp , err := h .client .GetChatCompletionStream (ctx , req , h .org )
342339 if err != nil {
343- if errors .Is (err , context .Canceled ) || strings .Contains (err .Error (), "EOF" ) {
344- break
340+ var rateLimitErr * azuremodels.RateLimitError
341+ if errors .As (err , & rateLimitErr ) {
342+ if attempt < maxRetries {
343+ if ! h .jsonOutput {
344+ h .cfg .WriteToOut (fmt .Sprintf (" Rate limited, waiting %v before retry (attempt %d/%d)...\n " ,
345+ rateLimitErr .RetryAfter , attempt + 1 , maxRetries + 1 ))
346+ }
347+
348+ // Wait for the specified duration
349+ select {
350+ case <- ctx .Done ():
351+ return "" , ctx .Err ()
352+ case <- time .After (rateLimitErr .RetryAfter ):
353+ continue
354+ }
355+ }
356+ return "" , fmt .Errorf ("rate limit exceeded after %d attempts: %w" , attempt + 1 , err )
345357 }
358+ // For non-rate-limit errors, return immediately
346359 return "" , err
347360 }
348361
349- for _ , choice := range completion .Choices {
350- if choice .Delta != nil && choice .Delta .Content != nil {
351- content .WriteString (* choice .Delta .Content )
362+ var content strings.Builder
363+ for {
364+ completion , err := resp .Reader .Read ()
365+ if err != nil {
366+ if errors .Is (err , context .Canceled ) || strings .Contains (err .Error (), "EOF" ) {
367+ break
368+ }
369+ return "" , err
352370 }
353- if choice .Message != nil && choice .Message .Content != nil {
354- content .WriteString (* choice .Message .Content )
371+
372+ for _ , choice := range completion .Choices {
373+ if choice .Delta != nil && choice .Delta .Content != nil {
374+ content .WriteString (* choice .Delta .Content )
375+ }
376+ if choice .Message != nil && choice .Message .Content != nil {
377+ content .WriteString (* choice .Message .Content )
378+ }
355379 }
356380 }
381+
382+ return strings .TrimSpace (content .String ()), nil
357383 }
358384
359- return strings .TrimSpace (content .String ()), nil
385+ // This should never be reached, but just in case
386+ return "" , errors .New ("unexpected error calling model" )
387+ }
388+
389+ func (h * evalCommandHandler ) callModel (ctx context.Context , messages []azuremodels.ChatMessage ) (string , error ) {
390+ req := h .evalFile .BuildChatCompletionOptions (messages )
391+ return h .callModelWithRetry (ctx , req )
360392}
361393
362394func (h * evalCommandHandler ) runEvaluators (ctx context.Context , testCase map [string ]interface {}, response string ) ([]EvaluationResult , error ) {
@@ -437,7 +469,6 @@ func (h *evalCommandHandler) runStringEvaluator(name string, eval prompt.StringE
437469}
438470
439471func (h * evalCommandHandler ) runLLMEvaluator (ctx context.Context , name string , eval prompt.LLMEvaluator , testCase map [string ]interface {}, response string ) (EvaluationResult , error ) {
440- // Template the evaluation prompt
441472 evalData := make (map [string ]interface {})
442473 for k , v := range testCase {
443474 evalData [k ] = v
@@ -449,7 +480,6 @@ func (h *evalCommandHandler) runLLMEvaluator(ctx context.Context, name string, e
449480 return EvaluationResult {}, fmt .Errorf ("failed to template evaluation prompt: %w" , err )
450481 }
451482
452- // Prepare messages for evaluation
453483 var messages []azuremodels.ChatMessage
454484 if eval .SystemPrompt != "" {
455485 messages = append (messages , azuremodels.ChatMessage {
@@ -462,40 +492,19 @@ func (h *evalCommandHandler) runLLMEvaluator(ctx context.Context, name string, e
462492 Content : util .Ptr (promptContent ),
463493 })
464494
465- // Call the evaluation model
466495 req := azuremodels.ChatCompletionOptions {
467496 Messages : messages ,
468497 Model : eval .ModelID ,
469498 Stream : false ,
470499 }
471500
472- resp , err := h .client . GetChatCompletionStream (ctx , req , h . org )
501+ evalResponseText , err := h .callModelWithRetry (ctx , req )
473502 if err != nil {
474503 return EvaluationResult {}, fmt .Errorf ("failed to call evaluation model: %w" , err )
475504 }
476505
477- var evalResponse strings.Builder
478- for {
479- completion , err := resp .Reader .Read ()
480- if err != nil {
481- if errors .Is (err , context .Canceled ) || strings .Contains (err .Error (), "EOF" ) {
482- break
483- }
484- return EvaluationResult {}, err
485- }
486-
487- for _ , choice := range completion .Choices {
488- if choice .Delta != nil && choice .Delta .Content != nil {
489- evalResponse .WriteString (* choice .Delta .Content )
490- }
491- if choice .Message != nil && choice .Message .Content != nil {
492- evalResponse .WriteString (* choice .Message .Content )
493- }
494- }
495- }
496-
497506 // Match response to choices
498- evalResponseText : = strings .TrimSpace (strings .ToLower (evalResponse . String () ))
507+ evalResponseText = strings .TrimSpace (strings .ToLower (evalResponseText ))
499508 for _ , choice := range eval .Choices {
500509 if strings .Contains (evalResponseText , strings .ToLower (choice .Choice )) {
501510 return EvaluationResult {
0 commit comments