@@ -75,6 +75,7 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine
7575 }
7676 }
7777
78+ var graderJSON json.RawMessage
7879 hyperparameters := & models.Hyperparameters {}
7980 if openaiJob .Method .Type == "supervised" {
8081 hyperparameters .BatchSize = openaiJob .Method .Supervised .Hyperparameters .BatchSize .OfInt
@@ -95,6 +96,14 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine
9596 if openaiJob .Method .Reinforcement .Hyperparameters .ReasoningEffort != "" {
9697 hyperparameters .ReasoningEffort = string (openaiJob .Method .Reinforcement .Hyperparameters .ReasoningEffort )
9798 }
99+ // Extract grader using the common function
100+ graderData := ExtractGraderFromOpenAI (openaiJob .Method .Reinforcement .Grader )
101+ if graderData != nil {
102+ graderBytes , err := json .Marshal (graderData )
103+ if err == nil {
104+ graderJSON = graderBytes
105+ }
106+ }
98107 } else {
99108 // Fallback to top-level hyperparameters (for backward compatibility)
100109 openaiJob .Method .Type = "supervised"
@@ -131,6 +140,7 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine
131140 TrainingFile : openaiJob .TrainingFile ,
132141 ValidationFile : openaiJob .ValidationFile ,
133142 Hyperparameters : hyperparameters ,
143+ Grader : graderJSON ,
134144 Seed : openaiJob .Seed ,
135145 ExtraFields : extraFields ,
136146 }
@@ -352,19 +362,9 @@ func convertInternalJobParamToOpenAiJobParams(config *models.CreateFineTuningReq
352362 }
353363
354364 grader := config .Method .Reinforcement .Grader
355- if grader != nil {
356- // Convert grader to JSON and unmarshal to ReinforcementMethodGraderUnionParam
357- graderJSON , err := json .Marshal (grader )
358- if err != nil {
359- return nil , nil , err
360- }
361-
362- var graderUnion openai.ReinforcementMethodGraderUnionParam
363- err = json .Unmarshal (graderJSON , & graderUnion )
364- if err != nil {
365- return nil , nil , err
366- }
367- reinforcementMethod .Grader = graderUnion
365+ if grader != nil && len (grader ) > 0 {
366+ // Convert grader map to SDK param type using the common function
367+ reinforcementMethod .Grader = ConvertGraderMapToSDKParam (grader )
368368 }
369369
370370 jobParams .Method = openai.FineTuningJobNewParamsMethod {
@@ -463,3 +463,213 @@ func getReasoningEffortValue(effort string) openai.ReinforcementHyperparametersR
463463 return openai .ReinforcementHyperparametersReasoningEffortDefault
464464 }
465465}
466+
467+ // ExtractGraderFromOpenAI extracts grader data from OpenAI SDK response to a clean map
468+ // This is used when cloning a job to YAML - only extracts relevant fields per grader type
469+ func ExtractGraderFromOpenAI (grader openai.ReinforcementMethodGraderUnion ) map [string ]interface {} {
470+ if grader .Type == "" {
471+ return nil
472+ }
473+
474+ graderType := grader .Type
475+ var graderData map [string ]interface {}
476+
477+ switch graderType {
478+ case "python" :
479+ g := grader .AsPythonGrader ()
480+ graderData = map [string ]interface {}{
481+ "type" : graderType ,
482+ "name" : g .Name ,
483+ "source" : g .Source ,
484+ }
485+ if g .ImageTag != "" {
486+ graderData ["image_tag" ] = g .ImageTag
487+ }
488+ case "string_check" :
489+ g := grader .AsStringCheckGrader ()
490+ graderData = map [string ]interface {}{
491+ "type" : graderType ,
492+ "input" : g .Input ,
493+ "name" : g .Name ,
494+ "operation" : string (g .Operation ),
495+ "reference" : g .Reference ,
496+ }
497+ case "text_similarity" :
498+ g := grader .AsTextSimilarityGrader ()
499+ graderData = map [string ]interface {}{
500+ "type" : graderType ,
501+ "input" : g .Input ,
502+ "name" : g .Name ,
503+ "reference" : g .Reference ,
504+ "evaluation_metric" : string (g .EvaluationMetric ),
505+ }
506+ case "score_model" :
507+ g := grader .AsScoreModelGrader ()
508+ graderData = map [string ]interface {}{
509+ "type" : graderType ,
510+ "input" : g .Input ,
511+ "name" : g .Name ,
512+ "model" : g .Model ,
513+ }
514+ // Extract sampling params if present
515+ samplingData := map [string ]interface {}{}
516+ if g .SamplingParams .Temperature != 0 {
517+ samplingData ["temperature" ] = g .SamplingParams .Temperature
518+ }
519+ if g .SamplingParams .TopP != 0 {
520+ samplingData ["top_p" ] = g .SamplingParams .TopP
521+ }
522+ if g .SamplingParams .MaxCompletionsTokens != 0 {
523+ samplingData ["max_completion_tokens" ] = g .SamplingParams .MaxCompletionsTokens
524+ }
525+ if g .SamplingParams .Seed != 0 {
526+ samplingData ["seed" ] = g .SamplingParams .Seed
527+ }
528+ if len (samplingData ) > 0 {
529+ graderData ["sampling_params" ] = samplingData
530+ }
531+ case "multi" :
532+ g := grader .AsMultiGrader ()
533+ graderData = map [string ]interface {}{
534+ "type" : graderType ,
535+ "name" : g .Name ,
536+ "calculate_output" : g .CalculateOutput ,
537+ }
538+ // Note: Multi-grader sub-graders extraction is complex due to SDK union flattening
539+ // For now, we store just the top-level multi-grader fields
540+ // Users may need to manually add sub-graders in the YAML
541+ }
542+
543+ return graderData
544+ }
545+
546+ // ConvertGraderMapToSDKParam converts a grader map (from YAML or extracted) to OpenAI SDK param type
547+ // This is the reverse operation - used when creating a job from config
548+ func ConvertGraderMapToSDKParam (graderMap map [string ]interface {}) openai.ReinforcementMethodGraderUnionParam {
549+ if graderMap == nil {
550+ return openai.ReinforcementMethodGraderUnionParam {}
551+ }
552+
553+ graderType , _ := graderMap ["type" ].(string )
554+
555+ switch graderType {
556+ case "python" :
557+ grader := openai.PythonGraderParam {
558+ Name : getString (graderMap , "name" ),
559+ Source : getString (graderMap , "source" ),
560+ }
561+ if imageTag := getString (graderMap , "image_tag" ); imageTag != "" {
562+ grader .ImageTag = openai .Opt (imageTag )
563+ }
564+ return openai.ReinforcementMethodGraderUnionParam {OfPythonGrader : & grader }
565+
566+ case "string_check" :
567+ grader := openai.StringCheckGraderParam {
568+ Input : getString (graderMap , "input" ),
569+ Name : getString (graderMap , "name" ),
570+ Operation : openai .StringCheckGraderOperation (getString (graderMap , "operation" )),
571+ Reference : getString (graderMap , "reference" ),
572+ }
573+ return openai.ReinforcementMethodGraderUnionParam {OfStringCheckGrader : & grader }
574+
575+ case "text_similarity" :
576+ grader := openai.TextSimilarityGraderParam {
577+ Input : getString (graderMap , "input" ),
578+ Name : getString (graderMap , "name" ),
579+ Reference : getString (graderMap , "reference" ),
580+ EvaluationMetric : openai .TextSimilarityGraderEvaluationMetric (getString (graderMap , "evaluation_metric" )),
581+ }
582+ return openai.ReinforcementMethodGraderUnionParam {OfTextSimilarityGrader : & grader }
583+
584+ case "score_model" :
585+ grader := openai.ScoreModelGraderParam {
586+ Input : getScoreModelInput (graderMap , "input" ),
587+ Name : getString (graderMap , "name" ),
588+ Model : getString (graderMap , "model" ),
589+ }
590+ // Handle sampling parameters
591+ if samplingMap , ok := graderMap ["sampling_params" ].(map [string ]interface {}); ok {
592+ if temp := getFloat (samplingMap , "temperature" ); temp != nil {
593+ grader .SamplingParams .Temperature = openai .Opt (* temp )
594+ }
595+ if topP := getFloat (samplingMap , "top_p" ); topP != nil {
596+ grader .SamplingParams .TopP = openai .Opt (* topP )
597+ }
598+ if maxTokens := getInt (samplingMap , "max_completion_tokens" ); maxTokens != nil {
599+ grader .SamplingParams .MaxCompletionsTokens = openai .Opt (* maxTokens )
600+ }
601+ if seed := getInt (samplingMap , "seed" ); seed != nil {
602+ grader .SamplingParams .Seed = openai .Opt (* seed )
603+ }
604+ }
605+ return openai.ReinforcementMethodGraderUnionParam {OfScoreModelGrader : & grader }
606+
607+ case "multi" :
608+ // Multi-grader requires complex nested grader structure
609+ // For now, return empty - users should define multi-graders directly in config
610+ // with the full structure if needed
611+ return openai.ReinforcementMethodGraderUnionParam {}
612+ }
613+
614+ return openai.ReinforcementMethodGraderUnionParam {}
615+ }
616+
617+ // Helper functions for safe type conversions
618+ func getString (m map [string ]interface {}, key string ) string {
619+ if v , ok := m [key ].(string ); ok {
620+ return v
621+ }
622+ return ""
623+ }
624+
625+ // getScoreModelInput converts input data to ScoreModelGraderInputParam slice
626+ func getScoreModelInput (m map [string ]interface {}, key string ) []openai.ScoreModelGraderInputParam {
627+ result := []openai.ScoreModelGraderInputParam {}
628+ if v , ok := m [key ].([]interface {}); ok {
629+ for _ , item := range v {
630+ if itemMap , ok := item .(map [string ]interface {}); ok {
631+ inputParam := openai.ScoreModelGraderInputParam {
632+ Role : getString (itemMap , "role" ),
633+ }
634+ if content := getString (itemMap , "content" ); content != "" {
635+ inputParam .Content = openai.ScoreModelGraderInputContentUnionParam {
636+ OfString : openai .String (content ),
637+ }
638+ }
639+ if itemType := getString (itemMap , "type" ); itemType != "" {
640+ inputParam .Type = itemType
641+ }
642+ result = append (result , inputParam )
643+ }
644+ }
645+ }
646+ return result
647+ }
648+
649+ func getFloat (m map [string ]interface {}, key string ) * float64 {
650+ switch v := m [key ].(type ) {
651+ case float64 :
652+ return & v
653+ case int :
654+ f := float64 (v )
655+ return & f
656+ case int64 :
657+ f := float64 (v )
658+ return & f
659+ }
660+ return nil
661+ }
662+
663+ func getInt (m map [string ]interface {}, key string ) * int64 {
664+ switch v := m [key ].(type ) {
665+ case int :
666+ i := int64 (v )
667+ return & i
668+ case int64 :
669+ return & v
670+ case float64 :
671+ i := int64 (v )
672+ return & i
673+ }
674+ return nil
675+ }
0 commit comments