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/4] 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/4] 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/4] 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/4] 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