Skip to content

Commit 0bb5928

Browse files
authored
RFT grader fixes (#6629)
* adding project-endpoint to init command * adding 12 preview release * fixig default customization method & adding training type * correct version * adding grader to cloning code * adding missing grader functionality * merging master
1 parent 4a87862 commit 0bb5928

6 files changed

Lines changed: 249 additions & 16 deletions

File tree

cli/azd/extensions/azure.ai.finetune/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# Release History
22

33

4-
## 0.0.13-preview (2026-01-28)
4+
## 0.0.14-preview (2026-01-28)
55

66
- Defaulting to supervise when fine tuning method is not return by API
77
- Adding training Type when cloning a job
8+
- Adding details of grader to cloning process.
9+
- Allow to submit a job with different graders in RFT.
810

911
## 0.0.12-preview (2026-01-23)
1012

cli/azd/extensions/azure.ai.finetune/extension.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ namespace: ai.finetuning
33
displayName: Foundry Fine Tuning (Preview)
44
description: Extension for Foundry Fine Tuning. (Preview)
55
usage: azd ai finetuning <command> [options]
6-
version: 0.0.13-preview
6+
version: 0.0.14-preview
77
language: go
88
capabilities:
99
- custom-commands

cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/azure/azure-dev/cli/azd/pkg/input"
2424
"github.com/azure/azure-dev/cli/azd/pkg/tools/github"
2525
"github.com/azure/azure-dev/cli/azd/pkg/ux"
26+
"github.com/braydonk/yaml"
2627
"github.com/fatih/color"
2728
"github.com/spf13/cobra"
2829

@@ -901,6 +902,24 @@ method:
901902
}
902903
}
903904

905+
// Add grader for reinforcement method if present
906+
if len(job.Grader) > 0 && strings.ToLower(job.Method) == "reinforcement" {
907+
var graderMap map[string]interface{}
908+
if err := json.Unmarshal(job.Grader, &graderMap); err == nil {
909+
graderYaml, err := yaml.Marshal(graderMap)
910+
if err == nil {
911+
// Indent the grader YAML to be nested under method.reinforcement.grader
912+
indentedGrader := ""
913+
for _, line := range strings.Split(string(graderYaml), "\n") {
914+
if line != "" {
915+
indentedGrader += " " + line + "\n"
916+
}
917+
}
918+
yamlContent += " grader:\n" + indentedGrader
919+
}
920+
}
921+
}
922+
904923
// Add training and validation files
905924
yamlContent += fmt.Sprintf("training_file: %s\n", job.TrainingFile)
906925
if job.ValidationFile != "" {

cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go

Lines changed: 223 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

cli/azd/extensions/azure.ai.finetune/pkg/models/finetune.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package models
55

66
import (
7+
"encoding/json"
78
"fmt"
89
"time"
910
)
@@ -127,6 +128,7 @@ type FineTuningJobDetail struct {
127128
TrainingFile string `json:"training_file" yaml:"training_file"`
128129
ValidationFile string `json:"validation_file,omitempty" yaml:"validation_file,omitempty"`
129130
Hyperparameters *Hyperparameters `json:"hyperparameters" yaml:"hyperparameters"`
131+
Grader json.RawMessage `json:"grader,omitempty" yaml:"grader,omitempty"`
130132
VendorMetadata map[string]interface{} `json:"-" yaml:"-"`
131133
Seed int64 `json:"-" yaml:"-"`
132134
ExtraFields map[string]interface{} `json:"extra_fields,omitempty" yaml:"extra_fields,omitempty"`
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.0.13-preview
1+
0.0.14-preview

0 commit comments

Comments
 (0)