From fe13cbc4ef8875c420ed707799115fcdd371867a Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:37:02 +0530 Subject: [PATCH 1/7] adding project-endpoint to init command --- .../extensions/azure.ai.finetune/CHANGELOG.md | 4 + .../azure.ai.finetune/extension.yaml | 2 +- .../azure.ai.finetune/internal/cmd/init.go | 176 +++++++++++++++++- .../extensions/azure.ai.finetune/version.txt | 2 +- 4 files changed, 174 insertions(+), 10 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md index 7cd9eb3e675..96a1a179867 100644 --- a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## 0.0.12-preview (2026-01-23) + +- Add Project-endpoint parameter to init command + ## 0.0.11-preview (2026-01-22) - Add metadata capability diff --git a/cli/azd/extensions/azure.ai.finetune/extension.yaml b/cli/azd/extensions/azure.ai.finetune/extension.yaml index 99ced275c68..3cb5d2d19f6 100644 --- a/cli/azd/extensions/azure.ai.finetune/extension.yaml +++ b/cli/azd/extensions/azure.ai.finetune/extension.yaml @@ -3,7 +3,7 @@ namespace: ai.finetuning displayName: Foundry Fine Tuning (Preview) description: Extension for Foundry Fine Tuning. (Preview) usage: azd ai finetuning [options] -version: 0.0.11-preview +version: 0.0.12-preview language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go b/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go index 203d692951e..d0f7bcb7814 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go @@ -22,6 +22,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/tools/github" + "github.com/azure/azure-dev/cli/azd/pkg/ux" "github.com/fatih/color" "github.com/spf13/cobra" @@ -32,6 +33,8 @@ type initFlags struct { rootFlagsDefinition template string projectResourceId string + subscriptionId string + projectEndpoint string jobId string src string env string @@ -137,16 +140,22 @@ func newInitCommand(rootFlags rootFlagsDefinition) *cobra.Command { cmd.Flags().StringVarP(&flags.template, "template", "t", "", "URL or path to a fine-tune job template") - cmd.Flags().StringVarP(&flags.projectResourceId, "project", "p", "", - "Existing Microsoft Foundry Project Id to initialize your azd environment with") + cmd.Flags().StringVarP(&flags.projectResourceId, "project-resource-id", "p", "", + "ARM resource ID of the Microsoft Foundry Project (e.g., /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{account}/projects/{project})") - cmd.Flags().StringVarP(&flags.src, "source", "s", "", + cmd.Flags().StringVarP(&flags.subscriptionId, "subscription", "s", "", + "Azure subscription ID") + + cmd.Flags().StringVarP(&flags.projectEndpoint, "project-endpoint", "e", "", + "Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)") + + cmd.Flags().StringVarP(&flags.src, "working-directory", "w", "", "Local path for project output") cmd.Flags().StringVarP(&flags.jobId, "from-job", "j", "", "Clone configuration from an existing job ID") - cmd.Flags().StringVarP(&flags.env, "environment", "e", "", "The name of the azd environment to use.") + cmd.Flags().StringVarP(&flags.env, "environment", "n", "", "The name of the azd environment to use.") return cmd } @@ -181,6 +190,98 @@ func extractProjectDetails(projectResourceId string) (*FoundryProject, error) { }, nil } +// parseProjectEndpoint extracts account name and project name from an endpoint URL +// Example: https://account-name.services.ai.azure.com/api/projects/project-name +func parseProjectEndpoint(endpoint string) (accountName string, projectName string, err error) { + parsedURL, err := url.Parse(endpoint) + if err != nil { + return "", "", fmt.Errorf("failed to parse endpoint URL: %w", err) + } + + // Extract account name from hostname (e.g., "account-name.services.ai.azure.com") + hostname := parsedURL.Hostname() + hostParts := strings.Split(hostname, ".") + if len(hostParts) < 1 || hostParts[0] == "" { + return "", "", fmt.Errorf("invalid endpoint URL: cannot extract account name from hostname") + } + accountName = hostParts[0] + + // Extract project name from path (e.g., "/api/projects/project-name") + pathParts := strings.Split(strings.Trim(parsedURL.Path, "/"), "/") + // Expected path: api/projects/{project-name} + if len(pathParts) >= 3 && pathParts[0] == "api" && pathParts[1] == "projects" { + projectName = pathParts[2] + } else { + return "", "", fmt.Errorf("invalid endpoint URL: cannot extract project name from path. Expected format: /api/projects/{project-name}") + } + + return accountName, projectName, nil +} + +// findProjectByEndpoint searches for a Foundry project matching the endpoint URL +func findProjectByEndpoint( + ctx context.Context, + subscriptionId string, + accountName string, + projectName string, + credential azcore.TokenCredential, +) (*FoundryProject, error) { + // Create Cognitive Services Accounts client to search for the account + accountsClient, err := armcognitiveservices.NewAccountsClient(subscriptionId, credential, nil) + if err != nil { + return nil, fmt.Errorf("failed to create Cognitive Services Accounts client: %w", err) + } + + // List all accounts in the subscription and find the matching one + pager := accountsClient.NewListPager(nil) + var foundAccount *armcognitiveservices.Account + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list Cognitive Services accounts: %w", err) + } + for _, account := range page.Value { + if account.Name != nil && strings.EqualFold(*account.Name, accountName) { + foundAccount = account + break + } + } + if foundAccount != nil { + break + } + } + + if foundAccount == nil { + return nil, fmt.Errorf("could not find Cognitive Services account '%s' in subscription '%s'", accountName, subscriptionId) + } + + // Parse the account's resource ID to get resource group + accountResourceId, err := arm.ParseResourceID(*foundAccount.ID) + if err != nil { + return nil, fmt.Errorf("failed to parse account resource ID: %w", err) + } + + // Create Projects client to verify the project exists + projectsClient, err := armcognitiveservices.NewProjectsClient(subscriptionId, credential, nil) + if err != nil { + return nil, fmt.Errorf("failed to create Cognitive Services Projects client: %w", err) + } + + // Get the project to verify it exists and get its details + projectResp, err := projectsClient.Get(ctx, accountResourceId.ResourceGroupName, accountName, projectName, nil) + if err != nil { + return nil, fmt.Errorf("could not find project '%s' under account '%s': %w", projectName, accountName, err) + } + + return &FoundryProject{ + SubscriptionId: subscriptionId, + ResourceGroupName: accountResourceId.ResourceGroupName, + AiAccountName: accountName, + AiProjectName: projectName, + Location: *projectResp.Location, + }, nil +} + func getExistingEnvironment(ctx context.Context, name *string, azdClient *azdext.AzdClient) (*azdext.Environment, error) { var env *azdext.Environment if name == nil || *name == "" { @@ -205,8 +306,67 @@ func getExistingEnvironment(ctx context.Context, name *string, azdClient *azdext func ensureEnvironment(ctx context.Context, flags *initFlags, azdClient *azdext.AzdClient) (*azdext.Environment, error) { var foundryProject *FoundryProject - // Parse the Microsoft Foundry project resource ID if provided & Fetch Tenant Id and Location using parsed information - if flags.projectResourceId != "" { + // Handle project endpoint URL - extract account/project names and find the ARM resource + if flags.projectEndpoint != "" { + accountName, projectName, err := parseProjectEndpoint(flags.projectEndpoint) + if err != nil { + return nil, fmt.Errorf("failed to parse project endpoint: %w", err) + } + + fmt.Printf("Parsed endpoint - Account: %s, Project: %s\n", accountName, projectName) + + // Get subscription ID - either from flag or prompt + subscriptionId := flags.subscriptionId + var tenantId string + + if subscriptionId == "" { + fmt.Println("Subscription ID is required to find the project. Let's select one.") + subscriptionResponse, err := azdClient.Prompt().PromptSubscription(ctx, &azdext.PromptSubscriptionRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to prompt for subscription: %w", err) + } + subscriptionId = subscriptionResponse.Subscription.Id + tenantId = subscriptionResponse.Subscription.TenantId + } else { + // Get tenant ID from subscription + tenantResponse, err := azdClient.Account().LookupTenant(ctx, &azdext.LookupTenantRequest{ + SubscriptionId: subscriptionId, + }) + if err != nil { + return nil, fmt.Errorf("failed to get tenant ID: %w", err) + } + tenantId = tenantResponse.TenantId + } + + // Create credential + credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{ + TenantID: tenantId, + AdditionallyAllowedTenants: []string{"*"}, + }) + if err != nil { + return nil, fmt.Errorf("failed to create Azure credential: %w", err) + } + + // Find the project by searching the subscription + spinner := ux.NewSpinner(&ux.SpinnerOptions{ + Text: fmt.Sprintf("Searching for project in subscription %s...", subscriptionId), + }) + if err := spinner.Start(ctx); err != nil { + fmt.Printf("failed to start spinner: %v\n", err) + } + + foundryProject, err = findProjectByEndpoint(ctx, subscriptionId, accountName, projectName, credential) + _ = spinner.Stop(ctx) + if err != nil { + return nil, fmt.Errorf("failed to find project from endpoint: %w", err) + } + foundryProject.TenantId = tenantId + + fmt.Printf("Found project - Resource Group: %s, Account: %s, Project: %s\n", + foundryProject.ResourceGroupName, foundryProject.AiAccountName, foundryProject.AiProjectName) + + } else if flags.projectResourceId != "" { + // Parse the Microsoft Foundry project resource ID if provided & Fetch Tenant Id and Location using parsed information var err error foundryProject, err = extractProjectDetails(flags.projectResourceId) if err != nil { @@ -258,7 +418,7 @@ func ensureEnvironment(ctx context.Context, flags *initFlags, azdClient *azdext. envArgs = append(envArgs, flags.env) } - if flags.projectResourceId != "" { + if foundryProject != nil { envArgs = append(envArgs, "--subscription", foundryProject.SubscriptionId) envArgs = append(envArgs, "--location", foundryProject.Location) } @@ -287,7 +447,7 @@ func ensureEnvironment(ctx context.Context, flags *initFlags, azdClient *azdext. } // Set TenantId, SubscriptionId, ResourceGroupName, AiAccountName, and Location in the environment - if flags.projectResourceId != "" { + if foundryProject != nil { _, err := azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{ EnvName: existingEnv.Name, diff --git a/cli/azd/extensions/azure.ai.finetune/version.txt b/cli/azd/extensions/azure.ai.finetune/version.txt index d421c52e7d2..d07831535ee 100644 --- a/cli/azd/extensions/azure.ai.finetune/version.txt +++ b/cli/azd/extensions/azure.ai.finetune/version.txt @@ -1 +1 @@ -0.0.11-preview +0.0.12-preview From 686161ad6b551a1e203040cb12e09ae041251d72 Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:50:33 +0530 Subject: [PATCH 2/7] adding 12 preview release --- cli/azd/extensions/registry.json | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/cli/azd/extensions/registry.json b/cli/azd/extensions/registry.json index e5e3db444fc..876e613783f 100644 --- a/cli/azd/extensions/registry.json +++ b/cli/azd/extensions/registry.json @@ -2276,6 +2276,76 @@ "url": "https://github.com/Azure/azure-dev/releases/download/azd-ext-azure-ai-finetune_0.0.11-preview/azure-ai-finetune-windows-arm64.zip" } } + }, + { + "capabilities": [ + "custom-commands", + "metadata" + ], + "version": "0.0.12-preview", + "usage": "azd ai finetuning \u003ccommand\u003e [options]", + "examples": [ + { + "name": "init", + "description": "Initialize a new AI fine-tuning project.", + "usage": "azd ai finetuning init" + }, + { + "name": "deploy", + "description": "Deploy AI fine-tuning job to Azure.", + "usage": "azd ai finetuning deploy" + } + ], + "artifacts": { + "darwin/amd64": { + "checksum": { + "algorithm": "sha256", + "value": "ff7f95ae6d5c76bccf2bf8163cd6567623c4ed146c7f407861d659f731735fce" + }, + "entryPoint": "azure-ai-finetune-darwin-amd64", + "url": "https://github.com/Azure/azure-dev/releases/download/azd-ext-azure-ai-finetune_0.0.12-preview/azure-ai-finetune-darwin-amd64.zip" + }, + "darwin/arm64": { + "checksum": { + "algorithm": "sha256", + "value": "f76f01ac2589a3a093d15b0764cfa76fdc31a07f1f4b275f02747597ebcb4bd5" + }, + "entryPoint": "azure-ai-finetune-darwin-arm64", + "url": "https://github.com/Azure/azure-dev/releases/download/azd-ext-azure-ai-finetune_0.0.12-preview/azure-ai-finetune-darwin-arm64.zip" + }, + "linux/amd64": { + "checksum": { + "algorithm": "sha256", + "value": "f896747ec311826af1f00acaf5817ec66ca4d80f6b50b93ceaf6c335f172cc99" + }, + "entryPoint": "azure-ai-finetune-linux-amd64", + "url": "https://github.com/Azure/azure-dev/releases/download/azd-ext-azure-ai-finetune_0.0.12-preview/azure-ai-finetune-linux-amd64.tar.gz" + }, + "linux/arm64": { + "checksum": { + "algorithm": "sha256", + "value": "1b6010d393fd8ccc737d76f2f39d16001ded8fd162850af1b04179b1d56283a4" + }, + "entryPoint": "azure-ai-finetune-linux-arm64", + "url": "https://github.com/Azure/azure-dev/releases/download/azd-ext-azure-ai-finetune_0.0.12-preview/azure-ai-finetune-linux-arm64.tar.gz" + }, + "windows/amd64": { + "checksum": { + "algorithm": "sha256", + "value": "e2ad79ef1095ec218dda38f01360572e36ebaba98da415e2d18fa42cd9681a7b" + }, + "entryPoint": "azure-ai-finetune-windows-amd64.exe", + "url": "https://github.com/Azure/azure-dev/releases/download/azd-ext-azure-ai-finetune_0.0.12-preview/azure-ai-finetune-windows-amd64.zip" + }, + "windows/arm64": { + "checksum": { + "algorithm": "sha256", + "value": "52d6cfc59a3c5490bf675704b7f3b1693f463ee8474366b91c35f154b0c32df8" + }, + "entryPoint": "azure-ai-finetune-windows-arm64.exe", + "url": "https://github.com/Azure/azure-dev/releases/download/azd-ext-azure-ai-finetune_0.0.12-preview/azure-ai-finetune-windows-arm64.zip" + } + } } ] } From 194c0189f1b2c008b0929af9450eda766149ecdd Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:36:36 +0530 Subject: [PATCH 3/7] fixig default customization method & adding training type --- cli/azd/extensions/azure.ai.finetune/CHANGELOG.md | 6 ++++++ .../extensions/azure.ai.finetune/extension.yaml | 2 +- .../azure.ai.finetune/internal/cmd/init.go | 5 +++++ .../internal/providers/openai/conversions.go | 15 ++++++++++++++- .../azure.ai.finetune/pkg/models/finetune.go | 3 ++- cli/azd/extensions/azure.ai.finetune/version.txt | 2 +- 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md index 96a1a179867..5faf2655fa3 100644 --- a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History + +## 0.0.12-preview (2026-01-28) + +- Defaulting to supervise when fine tuning method is not return by API +- Adding training Type when cloning a job + ## 0.0.12-preview (2026-01-23) - Add Project-endpoint parameter to init command diff --git a/cli/azd/extensions/azure.ai.finetune/extension.yaml b/cli/azd/extensions/azure.ai.finetune/extension.yaml index 3cb5d2d19f6..cfb4b741c4c 100644 --- a/cli/azd/extensions/azure.ai.finetune/extension.yaml +++ b/cli/azd/extensions/azure.ai.finetune/extension.yaml @@ -3,7 +3,7 @@ namespace: ai.finetuning displayName: Foundry Fine Tuning (Preview) description: Extension for Foundry Fine Tuning. (Preview) usage: azd ai finetuning [options] -version: 0.0.12-preview +version: 0.0.13-preview language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go b/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go index d0f7bcb7814..47324d0e526 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go @@ -907,6 +907,11 @@ method: yamlContent += fmt.Sprintf("validation_file: %s\n", job.ValidationFile) } + // Add extra_body with trainingType if present + if trainingType, ok := job.ExtraFields["trainingType"]; ok { + yamlContent += fmt.Sprintf("extra_body:\n trainingType: %v\n", trainingType) + } + // Determine the output directory (use src flag or current directory) outputDir := a.flags.src if outputDir == "" { diff --git a/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go b/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go index a1f08cf18d7..d6ca0055039 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go @@ -63,7 +63,18 @@ func convertOpenAIJobToModel(openaiJob openai.FineTuningJob) *models.FineTuningJ // convertOpenAIJobToDetailModel converts OpenAI SDK job to detailed domain model func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.FineTuningJobDetail { - // Extract hyperparameters from OpenAI job + // Extract extra fields from the API response + extraFields := make(map[string]interface{}) + for key, field := range openaiJob.JSON.ExtraFields { + // Parse the raw JSON value + var value interface{} + if err := json.Unmarshal([]byte(field.Raw()), &value); err == nil { + extraFields[key] = value + } else { + extraFields[key] = string(field.Raw()) + } + } + hyperparameters := &models.Hyperparameters{} if openaiJob.Method.Type == "supervised" { hyperparameters.BatchSize = openaiJob.Method.Supervised.Hyperparameters.BatchSize.OfInt @@ -86,6 +97,7 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine } } else { // Fallback to top-level hyperparameters (for backward compatibility) + openaiJob.Method.Type = "supervised" hyperparameters.BatchSize = openaiJob.Hyperparameters.BatchSize.OfInt hyperparameters.LearningRateMultiplier = openaiJob.Hyperparameters.LearningRateMultiplier.OfFloat hyperparameters.NEpochs = openaiJob.Hyperparameters.NEpochs.OfInt @@ -120,6 +132,7 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine ValidationFile: openaiJob.ValidationFile, Hyperparameters: hyperparameters, Seed: openaiJob.Seed, + ExtraFields: extraFields, } return jobDetail diff --git a/cli/azd/extensions/azure.ai.finetune/pkg/models/finetune.go b/cli/azd/extensions/azure.ai.finetune/pkg/models/finetune.go index e2f43c3b199..e67e558799e 100644 --- a/cli/azd/extensions/azure.ai.finetune/pkg/models/finetune.go +++ b/cli/azd/extensions/azure.ai.finetune/pkg/models/finetune.go @@ -123,12 +123,13 @@ type FineTuningJobDetail struct { CreatedAt time.Time `json:"created_at" yaml:"created_at"` FinishedAt *time.Time `json:"finished_at,omitempty" yaml:"finished_at,omitempty"` EstimatedFinish *time.Time `json:"estimated_finish,omitempty" yaml:"estimated_finish,omitempty"` - Method string `json:"training_type" yaml:"training_type"` + Method string `json:"method.type" yaml:"method.type"` TrainingFile string `json:"training_file" yaml:"training_file"` ValidationFile string `json:"validation_file,omitempty" yaml:"validation_file,omitempty"` Hyperparameters *Hyperparameters `json:"hyperparameters" yaml:"hyperparameters"` VendorMetadata map[string]interface{} `json:"-" yaml:"-"` Seed int64 `json:"-" yaml:"-"` + ExtraFields map[string]interface{} `json:"extra_fields,omitempty" yaml:"extra_fields,omitempty"` } // JobEvent represents an event associated with a fine-tuning job diff --git a/cli/azd/extensions/azure.ai.finetune/version.txt b/cli/azd/extensions/azure.ai.finetune/version.txt index d07831535ee..95d20fe702c 100644 --- a/cli/azd/extensions/azure.ai.finetune/version.txt +++ b/cli/azd/extensions/azure.ai.finetune/version.txt @@ -1 +1 @@ -0.0.12-preview +0.0.13-preview From fad0e4b22ada90c1b4d0ba62188678498a34c147 Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:37:33 +0530 Subject: [PATCH 4/7] correct version --- cli/azd/extensions/azure.ai.finetune/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md index 5faf2655fa3..726ea78e170 100644 --- a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md @@ -1,7 +1,7 @@ # Release History -## 0.0.12-preview (2026-01-28) +## 0.0.13-preview (2026-01-28) - Defaulting to supervise when fine tuning method is not return by API - Adding training Type when cloning a job From 692a8e73ae6d05bcfd00d9ef833783e870c2ba40 Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:11:14 +0530 Subject: [PATCH 5/7] adding grader to cloning code --- .../azure.ai.finetune/internal/cmd/init.go | 19 +++++++++++++++++++ .../internal/providers/openai/conversions.go | 9 +++++++++ .../azure.ai.finetune/pkg/models/finetune.go | 2 ++ 3 files changed, 30 insertions(+) diff --git a/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go b/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go index 47324d0e526..299fe55e37b 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/cmd/init.go @@ -23,6 +23,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/tools/github" "github.com/azure/azure-dev/cli/azd/pkg/ux" + "github.com/braydonk/yaml" "github.com/fatih/color" "github.com/spf13/cobra" @@ -901,6 +902,24 @@ method: } } + // Add grader for reinforcement method if present + if len(job.Grader) > 0 && strings.ToLower(job.Method) == "reinforcement" { + var graderMap map[string]interface{} + if err := json.Unmarshal(job.Grader, &graderMap); err == nil { + graderYaml, err := yaml.Marshal(graderMap) + if err == nil { + // Indent the grader YAML to be nested under method.reinforcement.grader + indentedGrader := "" + for _, line := range strings.Split(string(graderYaml), "\n") { + if line != "" { + indentedGrader += " " + line + "\n" + } + } + yamlContent += " grader:\n" + indentedGrader + } + } + } + // Add training and validation files yamlContent += fmt.Sprintf("training_file: %s\n", job.TrainingFile) if job.ValidationFile != "" { diff --git a/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go b/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go index d6ca0055039..9dce2e7524a 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go @@ -75,6 +75,7 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine } } + var graderJSON json.RawMessage hyperparameters := &models.Hyperparameters{} if openaiJob.Method.Type == "supervised" { hyperparameters.BatchSize = openaiJob.Method.Supervised.Hyperparameters.BatchSize.OfInt @@ -95,6 +96,13 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine if openaiJob.Method.Reinforcement.Hyperparameters.ReasoningEffort != "" { hyperparameters.ReasoningEffort = string(openaiJob.Method.Reinforcement.Hyperparameters.ReasoningEffort) } + // Marshal the entire Grader object to JSON + if openaiJob.Method.Reinforcement.Grader.Type != "" { + graderBytes, err := json.Marshal(openaiJob.Method.Reinforcement.Grader) + if err == nil { + graderJSON = graderBytes + } + } } else { // Fallback to top-level hyperparameters (for backward compatibility) openaiJob.Method.Type = "supervised" @@ -131,6 +139,7 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine TrainingFile: openaiJob.TrainingFile, ValidationFile: openaiJob.ValidationFile, Hyperparameters: hyperparameters, + Grader: graderJSON, Seed: openaiJob.Seed, ExtraFields: extraFields, } diff --git a/cli/azd/extensions/azure.ai.finetune/pkg/models/finetune.go b/cli/azd/extensions/azure.ai.finetune/pkg/models/finetune.go index e67e558799e..81fbe707788 100644 --- a/cli/azd/extensions/azure.ai.finetune/pkg/models/finetune.go +++ b/cli/azd/extensions/azure.ai.finetune/pkg/models/finetune.go @@ -4,6 +4,7 @@ package models import ( + "encoding/json" "fmt" "time" ) @@ -127,6 +128,7 @@ type FineTuningJobDetail struct { TrainingFile string `json:"training_file" yaml:"training_file"` ValidationFile string `json:"validation_file,omitempty" yaml:"validation_file,omitempty"` Hyperparameters *Hyperparameters `json:"hyperparameters" yaml:"hyperparameters"` + Grader json.RawMessage `json:"grader,omitempty" yaml:"grader,omitempty"` VendorMetadata map[string]interface{} `json:"-" yaml:"-"` Seed int64 `json:"-" yaml:"-"` ExtraFields map[string]interface{} `json:"extra_fields,omitempty" yaml:"extra_fields,omitempty"` From 07b51367ee32de62c5cab5188bba4b2e912e2581 Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:19:43 +0530 Subject: [PATCH 6/7] adding missing grader functionality --- .../extensions/azure.ai.finetune/CHANGELOG.md | 4 +- .../azure.ai.finetune/extension.yaml | 2 +- .../internal/providers/openai/conversions.go | 233 ++++++++++++++++-- .../extensions/azure.ai.finetune/version.txt | 2 +- 4 files changed, 222 insertions(+), 19 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md index 726ea78e170..c42751d3e58 100644 --- a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md @@ -1,10 +1,12 @@ # Release History -## 0.0.13-preview (2026-01-28) +## 0.0.14-preview (2026-01-28) - Defaulting to supervise when fine tuning method is not return by API - Adding training Type when cloning a job +- Adding details of grader to cloning process. +- Allow to submit a job with different graders in RFT. ## 0.0.12-preview (2026-01-23) diff --git a/cli/azd/extensions/azure.ai.finetune/extension.yaml b/cli/azd/extensions/azure.ai.finetune/extension.yaml index cfb4b741c4c..1acc8a27ff4 100644 --- a/cli/azd/extensions/azure.ai.finetune/extension.yaml +++ b/cli/azd/extensions/azure.ai.finetune/extension.yaml @@ -3,7 +3,7 @@ namespace: ai.finetuning displayName: Foundry Fine Tuning (Preview) description: Extension for Foundry Fine Tuning. (Preview) usage: azd ai finetuning [options] -version: 0.0.13-preview +version: 0.0.14-preview language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go b/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go index 9dce2e7524a..11ff4364687 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/providers/openai/conversions.go @@ -96,9 +96,10 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine if openaiJob.Method.Reinforcement.Hyperparameters.ReasoningEffort != "" { hyperparameters.ReasoningEffort = string(openaiJob.Method.Reinforcement.Hyperparameters.ReasoningEffort) } - // Marshal the entire Grader object to JSON - if openaiJob.Method.Reinforcement.Grader.Type != "" { - graderBytes, err := json.Marshal(openaiJob.Method.Reinforcement.Grader) + // Extract grader using the common function + graderData := ExtractGraderFromOpenAI(openaiJob.Method.Reinforcement.Grader) + if graderData != nil { + graderBytes, err := json.Marshal(graderData) if err == nil { graderJSON = graderBytes } @@ -361,19 +362,9 @@ func convertInternalJobParamToOpenAiJobParams(config *models.CreateFineTuningReq } grader := config.Method.Reinforcement.Grader - if grader != nil { - // Convert grader to JSON and unmarshal to ReinforcementMethodGraderUnionParam - graderJSON, err := json.Marshal(grader) - if err != nil { - return nil, nil, err - } - - var graderUnion openai.ReinforcementMethodGraderUnionParam - err = json.Unmarshal(graderJSON, &graderUnion) - if err != nil { - return nil, nil, err - } - reinforcementMethod.Grader = graderUnion + if grader != nil && len(grader) > 0 { + // Convert grader map to SDK param type using the common function + reinforcementMethod.Grader = ConvertGraderMapToSDKParam(grader) } jobParams.Method = openai.FineTuningJobNewParamsMethod{ @@ -472,3 +463,213 @@ func getReasoningEffortValue(effort string) openai.ReinforcementHyperparametersR return openai.ReinforcementHyperparametersReasoningEffortDefault } } + +// ExtractGraderFromOpenAI extracts grader data from OpenAI SDK response to a clean map +// This is used when cloning a job to YAML - only extracts relevant fields per grader type +func ExtractGraderFromOpenAI(grader openai.ReinforcementMethodGraderUnion) map[string]interface{} { + if grader.Type == "" { + return nil + } + + graderType := grader.Type + var graderData map[string]interface{} + + switch graderType { + case "python": + g := grader.AsPythonGrader() + graderData = map[string]interface{}{ + "type": graderType, + "name": g.Name, + "source": g.Source, + } + if g.ImageTag != "" { + graderData["image_tag"] = g.ImageTag + } + case "string_check": + g := grader.AsStringCheckGrader() + graderData = map[string]interface{}{ + "type": graderType, + "input": g.Input, + "name": g.Name, + "operation": string(g.Operation), + "reference": g.Reference, + } + case "text_similarity": + g := grader.AsTextSimilarityGrader() + graderData = map[string]interface{}{ + "type": graderType, + "input": g.Input, + "name": g.Name, + "reference": g.Reference, + "evaluation_metric": string(g.EvaluationMetric), + } + case "score_model": + g := grader.AsScoreModelGrader() + graderData = map[string]interface{}{ + "type": graderType, + "input": g.Input, + "name": g.Name, + "model": g.Model, + } + // Extract sampling params if present + samplingData := map[string]interface{}{} + if g.SamplingParams.Temperature != 0 { + samplingData["temperature"] = g.SamplingParams.Temperature + } + if g.SamplingParams.TopP != 0 { + samplingData["top_p"] = g.SamplingParams.TopP + } + if g.SamplingParams.MaxCompletionsTokens != 0 { + samplingData["max_completion_tokens"] = g.SamplingParams.MaxCompletionsTokens + } + if g.SamplingParams.Seed != 0 { + samplingData["seed"] = g.SamplingParams.Seed + } + if len(samplingData) > 0 { + graderData["sampling_params"] = samplingData + } + case "multi": + g := grader.AsMultiGrader() + graderData = map[string]interface{}{ + "type": graderType, + "name": g.Name, + "calculate_output": g.CalculateOutput, + } + // Note: Multi-grader sub-graders extraction is complex due to SDK union flattening + // For now, we store just the top-level multi-grader fields + // Users may need to manually add sub-graders in the YAML + } + + return graderData +} + +// ConvertGraderMapToSDKParam converts a grader map (from YAML or extracted) to OpenAI SDK param type +// This is the reverse operation - used when creating a job from config +func ConvertGraderMapToSDKParam(graderMap map[string]interface{}) openai.ReinforcementMethodGraderUnionParam { + if graderMap == nil { + return openai.ReinforcementMethodGraderUnionParam{} + } + + graderType, _ := graderMap["type"].(string) + + switch graderType { + case "python": + grader := openai.PythonGraderParam{ + Name: getString(graderMap, "name"), + Source: getString(graderMap, "source"), + } + if imageTag := getString(graderMap, "image_tag"); imageTag != "" { + grader.ImageTag = openai.Opt(imageTag) + } + return openai.ReinforcementMethodGraderUnionParam{OfPythonGrader: &grader} + + case "string_check": + grader := openai.StringCheckGraderParam{ + Input: getString(graderMap, "input"), + Name: getString(graderMap, "name"), + Operation: openai.StringCheckGraderOperation(getString(graderMap, "operation")), + Reference: getString(graderMap, "reference"), + } + return openai.ReinforcementMethodGraderUnionParam{OfStringCheckGrader: &grader} + + case "text_similarity": + grader := openai.TextSimilarityGraderParam{ + Input: getString(graderMap, "input"), + Name: getString(graderMap, "name"), + Reference: getString(graderMap, "reference"), + EvaluationMetric: openai.TextSimilarityGraderEvaluationMetric(getString(graderMap, "evaluation_metric")), + } + return openai.ReinforcementMethodGraderUnionParam{OfTextSimilarityGrader: &grader} + + case "score_model": + grader := openai.ScoreModelGraderParam{ + Input: getScoreModelInput(graderMap, "input"), + Name: getString(graderMap, "name"), + Model: getString(graderMap, "model"), + } + // Handle sampling parameters + if samplingMap, ok := graderMap["sampling_params"].(map[string]interface{}); ok { + if temp := getFloat(samplingMap, "temperature"); temp != nil { + grader.SamplingParams.Temperature = openai.Opt(*temp) + } + if topP := getFloat(samplingMap, "top_p"); topP != nil { + grader.SamplingParams.TopP = openai.Opt(*topP) + } + if maxTokens := getInt(samplingMap, "max_completion_tokens"); maxTokens != nil { + grader.SamplingParams.MaxCompletionsTokens = openai.Opt(*maxTokens) + } + if seed := getInt(samplingMap, "seed"); seed != nil { + grader.SamplingParams.Seed = openai.Opt(*seed) + } + } + return openai.ReinforcementMethodGraderUnionParam{OfScoreModelGrader: &grader} + + case "multi": + // Multi-grader requires complex nested grader structure + // For now, return empty - users should define multi-graders directly in config + // with the full structure if needed + return openai.ReinforcementMethodGraderUnionParam{} + } + + return openai.ReinforcementMethodGraderUnionParam{} +} + +// Helper functions for safe type conversions +func getString(m map[string]interface{}, key string) string { + if v, ok := m[key].(string); ok { + return v + } + return "" +} + +// getScoreModelInput converts input data to ScoreModelGraderInputParam slice +func getScoreModelInput(m map[string]interface{}, key string) []openai.ScoreModelGraderInputParam { + result := []openai.ScoreModelGraderInputParam{} + if v, ok := m[key].([]interface{}); ok { + for _, item := range v { + if itemMap, ok := item.(map[string]interface{}); ok { + inputParam := openai.ScoreModelGraderInputParam{ + Role: getString(itemMap, "role"), + } + if content := getString(itemMap, "content"); content != "" { + inputParam.Content = openai.ScoreModelGraderInputContentUnionParam{ + OfString: openai.String(content), + } + } + if itemType := getString(itemMap, "type"); itemType != "" { + inputParam.Type = itemType + } + result = append(result, inputParam) + } + } + } + return result +} + +func getFloat(m map[string]interface{}, key string) *float64 { + switch v := m[key].(type) { + case float64: + return &v + case int: + f := float64(v) + return &f + case int64: + f := float64(v) + return &f + } + return nil +} + +func getInt(m map[string]interface{}, key string) *int64 { + switch v := m[key].(type) { + case int: + i := int64(v) + return &i + case int64: + return &v + case float64: + i := int64(v) + return &i + } + return nil +} diff --git a/cli/azd/extensions/azure.ai.finetune/version.txt b/cli/azd/extensions/azure.ai.finetune/version.txt index 95d20fe702c..177e48acbcb 100644 --- a/cli/azd/extensions/azure.ai.finetune/version.txt +++ b/cli/azd/extensions/azure.ai.finetune/version.txt @@ -1 +1 @@ -0.0.13-preview +0.0.14-preview From 981a324d993de4ed09f4112b651548d88d410e72 Mon Sep 17 00:00:00 2001 From: Amit Chauhan <70937115+achauhan-scc@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:24:13 +0530 Subject: [PATCH 7/7] merging master --- cli/azd/extensions/azure.ai.finetune/CHANGELOG.md | 7 ------- cli/azd/extensions/azure.ai.finetune/extension.yaml | 4 ---- cli/azd/extensions/azure.ai.finetune/version.txt | 4 ---- 3 files changed, 15 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md index ae8d85eb771..c42751d3e58 100644 --- a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md @@ -1,19 +1,12 @@ # Release History -<<<<<<< HEAD ## 0.0.14-preview (2026-01-28) - Defaulting to supervise when fine tuning method is not return by API - Adding training Type when cloning a job - Adding details of grader to cloning process. - Allow to submit a job with different graders in RFT. -======= -## 0.0.13-preview (2026-01-28) - -- Defaulting to supervise when fine tuning method is not return by API -- Adding training Type when cloning a job ->>>>>>> 4a87862ee90fbda92faca7848c3c6d73f3580cc0 ## 0.0.12-preview (2026-01-23) diff --git a/cli/azd/extensions/azure.ai.finetune/extension.yaml b/cli/azd/extensions/azure.ai.finetune/extension.yaml index c698de6f313..1acc8a27ff4 100644 --- a/cli/azd/extensions/azure.ai.finetune/extension.yaml +++ b/cli/azd/extensions/azure.ai.finetune/extension.yaml @@ -3,11 +3,7 @@ namespace: ai.finetuning displayName: Foundry Fine Tuning (Preview) description: Extension for Foundry Fine Tuning. (Preview) usage: azd ai finetuning [options] -<<<<<<< HEAD version: 0.0.14-preview -======= -version: 0.0.13-preview ->>>>>>> 4a87862ee90fbda92faca7848c3c6d73f3580cc0 language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/azure.ai.finetune/version.txt b/cli/azd/extensions/azure.ai.finetune/version.txt index 03ddcd90aac..177e48acbcb 100644 --- a/cli/azd/extensions/azure.ai.finetune/version.txt +++ b/cli/azd/extensions/azure.ai.finetune/version.txt @@ -1,5 +1 @@ -<<<<<<< HEAD 0.0.14-preview -======= -0.0.13-preview ->>>>>>> 4a87862ee90fbda92faca7848c3c6d73f3580cc0