From 6e5c336779cb0ae545ba1a92e237b61b1c23a4c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:49:41 +0000 Subject: [PATCH 1/2] Initial plan From 3e6bbecc750d74c191c3e92dec01ad5ff24faa5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 05:08:56 +0000 Subject: [PATCH 2/2] Fix: Finetune Extension not honoring endpoint & subscription flags Co-authored-by: achauhan-scc <70937115+achauhan-scc@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/azure-dev/sessions/d39e7aad-25ba-44c2-a694-4ece1ba25656 --- .../extensions/azure.ai.finetune/CHANGELOG.md | 6 ++ .../azure.ai.finetune/extension.yaml | 2 +- .../internal/cmd/operations.go | 100 +++++++++++++----- .../internal/cmd/operations_test.go | 44 ++++---- .../internal/cmd/validation.go | 91 +++------------- .../providers/factory/provider_factory.go | 20 ++++ .../internal/services/finetune_service.go | 10 ++ .../extensions/azure.ai.finetune/version.txt | 2 +- 8 files changed, 151 insertions(+), 124 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md index 46e9978bd29..0e8a051de33 100644 --- a/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.finetune/CHANGELOG.md @@ -1,6 +1,12 @@ # Release History +## 0.0.18-preview (2026-03-23) + +- Fix: `--project-endpoint` and `--subscription` flags now take priority over any previously configured azd environment when running `jobs` commands (list, show, submit, pause, resume, cancel, deploy). +- Fix: Removed warning that incorrectly ignored user-provided flags when an environment was already configured. +- The priority order for endpoint resolution is now: (1) explicit flags, (2) azd environment variables, (3) error with guidance to run `azd ai finetuning init`. + ## 0.0.17-preview (2026-02-20) - Add multi-grader support for reinforcement fine-tuning diff --git a/cli/azd/extensions/azure.ai.finetune/extension.yaml b/cli/azd/extensions/azure.ai.finetune/extension.yaml index afcac71f338..2eea05b6047 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.17-preview +version: 0.0.18-preview language: go capabilities: - custom-commands diff --git a/cli/azd/extensions/azure.ai.finetune/internal/cmd/operations.go b/cli/azd/extensions/azure.ai.finetune/internal/cmd/operations.go index b1c54c68321..658ce769c81 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/cmd/operations.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/cmd/operations.go @@ -16,6 +16,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/ux" + "azure.ai.finetune/internal/providers" "azure.ai.finetune/internal/providers/factory" "azure.ai.finetune/internal/services" "azure.ai.finetune/internal/utils" @@ -40,22 +41,52 @@ func newOperationCommand() *cobra.Command { } cmd.PersistentFlags().StringVarP(&flags.subscriptionId, "subscription", "s", "", - "Azure subscription ID (enables implicit init if environment not configured)") + "Azure subscription ID. When provided together with --project-endpoint, takes priority over any configured azd environment.") cmd.PersistentFlags().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.AddCommand(newOperationSubmitCommand()) - cmd.AddCommand(newOperationShowCommand()) - cmd.AddCommand(newOperationListCommand()) - cmd.AddCommand(newOperationPauseCommand()) - cmd.AddCommand(newOperationResumeCommand()) - cmd.AddCommand(newOperationCancelCommand()) - cmd.AddCommand(newOperationDeployModelCommand()) + cmd.AddCommand(newOperationSubmitCommand(flags)) + cmd.AddCommand(newOperationShowCommand(flags)) + cmd.AddCommand(newOperationListCommand(flags)) + cmd.AddCommand(newOperationPauseCommand(flags)) + cmd.AddCommand(newOperationResumeCommand(flags)) + cmd.AddCommand(newOperationCancelCommand(flags)) + cmd.AddCommand(newOperationDeployModelCommand(flags)) return cmd } -func newOperationSubmitCommand() *cobra.Command { +// resolveFineTuningProvider creates a FineTuningProvider using the provided flags when available, +// falling back to azd environment configuration. This implements the priority order: +// (1) explicit --project-endpoint and --subscription flags, (2) azd environment variables. +func resolveFineTuningProvider( + ctx context.Context, + flags *jobsFlags, + azdClient *azdext.AzdClient, +) (providers.FineTuningProvider, error) { + if flags != nil && flags.projectEndpoint != "" { + tenantResp, err := azdClient.Account().LookupTenant(ctx, &azdext.LookupTenantRequest{ + SubscriptionId: flags.subscriptionId, + }) + if err != nil { + return nil, fmt.Errorf("failed to look up tenant for subscription %s: %w", flags.subscriptionId, err) + } + return factory.NewFineTuningProviderWithEndpoint(flags.projectEndpoint, tenantResp.TenantId) + } + return factory.NewFineTuningProvider(ctx, azdClient) +} + +// createFineTuningService creates a FineTuningService using the provided flags when available, +// falling back to azd environment configuration. +func createFineTuningService(ctx context.Context, flags *jobsFlags, azdClient *azdext.AzdClient) (services.FineTuningService, error) { + provider, err := resolveFineTuningProvider(ctx, flags, azdClient) + if err != nil { + return nil, fmt.Errorf("failed to create fine-tuning provider: %w", err) + } + return services.NewFineTuningServiceWithProvider(provider, nil), nil +} + +func newOperationSubmitCommand(flags *jobsFlags) *cobra.Command { var filename string var model string var trainingFile string @@ -125,14 +156,12 @@ func newOperationSubmitCommand() *cobra.Command { config.Seed = &seed } - fineTuneSvc, err := services.NewFineTuningService(ctx, azdClient, nil) + fineTuneSvc, err := createFineTuningService(ctx, flags, azdClient) if err != nil { _ = spinner.Stop(ctx) fmt.Println() return err } - - // Submit the fine-tuning job using CreateJob from JobWrapper job, err := fineTuneSvc.CreateFineTuningJob(ctx, config) _ = spinner.Stop(ctx) fmt.Println() @@ -171,7 +200,7 @@ func newOperationSubmitCommand() *cobra.Command { } // newOperationShowCommand creates a command to show the fine-tuning job details -func newOperationShowCommand() *cobra.Command { +func newOperationShowCommand(flags *jobsFlags) *cobra.Command { var jobID string var logs bool var output string @@ -207,7 +236,7 @@ func newOperationShowCommand() *cobra.Command { fmt.Printf("failed to start spinner: %v\n", err) } - fineTuneSvc, err := services.NewFineTuningService(ctx, azdClient, nil) + fineTuneSvc, err := createFineTuningService(ctx, flags, azdClient) if err != nil { _ = spinner.Stop(ctx) fmt.Print("\n\n") @@ -282,7 +311,7 @@ func newOperationShowCommand() *cobra.Command { } // newOperationListCommand creates a command to list fine-tuning jobs -func newOperationListCommand() *cobra.Command { +func newOperationListCommand(flags *jobsFlags) *cobra.Command { var limit int var after string var output string @@ -313,7 +342,7 @@ func newOperationListCommand() *cobra.Command { fmt.Printf("failed to start spinner: %v\n", err) } - fineTuneSvc, err := services.NewFineTuningService(ctx, azdClient, nil) + fineTuneSvc, err := createFineTuningService(ctx, flags, azdClient) if err != nil { _ = spinner.Stop(ctx) fmt.Println() @@ -347,7 +376,7 @@ func newOperationListCommand() *cobra.Command { } // newOperationPauseCommand creates a command to pause a running fine-tuning job -func newOperationPauseCommand() *cobra.Command { +func newOperationPauseCommand(flags *jobsFlags) *cobra.Command { var jobID string requiredFlag := "id" @@ -373,7 +402,7 @@ func newOperationPauseCommand() *cobra.Command { fmt.Printf("failed to start spinner: %v\n", err) } - fineTuneSvc, err := services.NewFineTuningService(ctx, azdClient, nil) + fineTuneSvc, err := createFineTuningService(ctx, flags, azdClient) if err != nil { _ = spinner.Stop(ctx) fmt.Println() @@ -406,7 +435,7 @@ func newOperationPauseCommand() *cobra.Command { } // newOperationResumeCommand creates a command to resume a paused fine-tuning job -func newOperationResumeCommand() *cobra.Command { +func newOperationResumeCommand(flags *jobsFlags) *cobra.Command { var jobID string requiredFlag := "id" @@ -432,7 +461,7 @@ func newOperationResumeCommand() *cobra.Command { fmt.Printf("failed to start spinner: %v\n", err) } - fineTuneSvc, err := services.NewFineTuningService(ctx, azdClient, nil) + fineTuneSvc, err := createFineTuningService(ctx, flags, azdClient) if err != nil { _ = spinner.Stop(ctx) fmt.Println() @@ -465,7 +494,7 @@ func newOperationResumeCommand() *cobra.Command { } // newOperationCancelCommand creates a command to cancel a fine-tuning job -func newOperationCancelCommand() *cobra.Command { +func newOperationCancelCommand(flags *jobsFlags) *cobra.Command { var jobID string var force bool requiredFlag := "id" @@ -504,7 +533,7 @@ func newOperationCancelCommand() *cobra.Command { fmt.Printf("failed to start spinner: %v\n", err) } - fineTuneSvc, err := services.NewFineTuningService(ctx, azdClient, nil) + fineTuneSvc, err := createFineTuningService(ctx, flags, azdClient) if err != nil { _ = spinner.Stop(ctx) fmt.Println() @@ -535,7 +564,7 @@ func newOperationCancelCommand() *cobra.Command { return cmd } -func newOperationDeployModelCommand() *cobra.Command { +func newOperationDeployModelCommand(flags *jobsFlags) *cobra.Command { var jobID string var deploymentName string var modelFormat string @@ -591,6 +620,27 @@ func newOperationDeployModelCommand() *cobra.Command { } } + // Apply flag overrides: flags take priority over environment variables. + if flags != nil && flags.subscriptionId != "" { + envValueMap["AZURE_SUBSCRIPTION_ID"] = flags.subscriptionId + } + if flags != nil && flags.projectEndpoint != "" { + accountName, _, err := parseProjectEndpoint(flags.projectEndpoint) + if err == nil && accountName != "" { + envValueMap["AZURE_ACCOUNT_NAME"] = accountName + } + } + + // Resolve tenant ID: look it up from subscription if not in env. + if envValueMap["AZURE_TENANT_ID"] == "" && envValueMap["AZURE_SUBSCRIPTION_ID"] != "" { + tenantResp, err := azdClient.Account().LookupTenant(ctx, &azdext.LookupTenantRequest{ + SubscriptionId: envValueMap["AZURE_SUBSCRIPTION_ID"], + }) + if err == nil { + envValueMap["AZURE_TENANT_ID"] = tenantResp.TenantId + } + } + // Validate required environment variables requiredEnvVars := []string{ "AZURE_SUBSCRIPTION_ID", @@ -632,8 +682,8 @@ func newOperationDeployModelCommand() *cobra.Command { return fmt.Errorf("failed to create azure credential: %w", err) } - // Initialize fine-tuning provider - ftProvider, err := factory.NewFineTuningProvider(ctx, azdClient) + // Initialize fine-tuning provider (honors flags if provided) + ftProvider, err := resolveFineTuningProvider(ctx, flags, azdClient) if err != nil { _ = spinner.Stop(ctx) fmt.Println() diff --git a/cli/azd/extensions/azure.ai.finetune/internal/cmd/operations_test.go b/cli/azd/extensions/azure.ai.finetune/internal/cmd/operations_test.go index 8d3c9cdfb9e..6472b430664 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/cmd/operations_test.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/cmd/operations_test.go @@ -49,7 +49,7 @@ func TestNewOperationCommand_HasAllSubcommands(t *testing.T) { } func TestNewOperationSubmitCommand(t *testing.T) { - cmd := newOperationSubmitCommand() + cmd := newOperationSubmitCommand(&jobsFlags{}) require.NotNil(t, cmd) require.Equal(t, "submit", cmd.Use) @@ -58,7 +58,7 @@ func TestNewOperationSubmitCommand(t *testing.T) { } func TestNewOperationSubmitCommand_Flags(t *testing.T) { - cmd := newOperationSubmitCommand() + cmd := newOperationSubmitCommand(&jobsFlags{}) // Test that all expected flags are defined expectedFlags := []struct { @@ -84,7 +84,7 @@ func TestNewOperationSubmitCommand_Flags(t *testing.T) { } func TestNewOperationShowCommand(t *testing.T) { - cmd := newOperationShowCommand() + cmd := newOperationShowCommand(&jobsFlags{}) require.NotNil(t, cmd) require.Equal(t, "show", cmd.Use) @@ -93,7 +93,7 @@ func TestNewOperationShowCommand(t *testing.T) { } func TestNewOperationShowCommand_Flags(t *testing.T) { - cmd := newOperationShowCommand() + cmd := newOperationShowCommand(&jobsFlags{}) expectedFlags := []struct { name string @@ -120,7 +120,7 @@ func TestNewOperationShowCommand_Flags(t *testing.T) { } func TestNewOperationListCommand(t *testing.T) { - cmd := newOperationListCommand() + cmd := newOperationListCommand(&jobsFlags{}) require.NotNil(t, cmd) require.Equal(t, "list", cmd.Use) @@ -129,7 +129,7 @@ func TestNewOperationListCommand(t *testing.T) { } func TestNewOperationListCommand_Flags(t *testing.T) { - cmd := newOperationListCommand() + cmd := newOperationListCommand(&jobsFlags{}) // top flag (limit) topFlag := cmd.Flags().Lookup("top") @@ -150,7 +150,7 @@ func TestNewOperationListCommand_Flags(t *testing.T) { } func TestNewOperationPauseCommand(t *testing.T) { - cmd := newOperationPauseCommand() + cmd := newOperationPauseCommand(&jobsFlags{}) require.NotNil(t, cmd) require.Equal(t, "pause", cmd.Use) @@ -160,7 +160,7 @@ func TestNewOperationPauseCommand(t *testing.T) { } func TestNewOperationPauseCommand_Flags(t *testing.T) { - cmd := newOperationPauseCommand() + cmd := newOperationPauseCommand(&jobsFlags{}) idFlag := cmd.Flags().Lookup("id") require.NotNil(t, idFlag) @@ -168,7 +168,7 @@ func TestNewOperationPauseCommand_Flags(t *testing.T) { } func TestNewOperationResumeCommand(t *testing.T) { - cmd := newOperationResumeCommand() + cmd := newOperationResumeCommand(&jobsFlags{}) require.NotNil(t, cmd) require.Equal(t, "resume", cmd.Use) @@ -178,7 +178,7 @@ func TestNewOperationResumeCommand(t *testing.T) { } func TestNewOperationResumeCommand_Flags(t *testing.T) { - cmd := newOperationResumeCommand() + cmd := newOperationResumeCommand(&jobsFlags{}) idFlag := cmd.Flags().Lookup("id") require.NotNil(t, idFlag) @@ -186,7 +186,7 @@ func TestNewOperationResumeCommand_Flags(t *testing.T) { } func TestNewOperationCancelCommand(t *testing.T) { - cmd := newOperationCancelCommand() + cmd := newOperationCancelCommand(&jobsFlags{}) require.NotNil(t, cmd) require.Equal(t, "cancel", cmd.Use) @@ -196,7 +196,7 @@ func TestNewOperationCancelCommand(t *testing.T) { } func TestNewOperationCancelCommand_Flags(t *testing.T) { - cmd := newOperationCancelCommand() + cmd := newOperationCancelCommand(&jobsFlags{}) idFlag := cmd.Flags().Lookup("id") require.NotNil(t, idFlag) @@ -208,7 +208,7 @@ func TestNewOperationCancelCommand_Flags(t *testing.T) { } func TestNewOperationDeployModelCommand(t *testing.T) { - cmd := newOperationDeployModelCommand() + cmd := newOperationDeployModelCommand(&jobsFlags{}) require.NotNil(t, cmd) require.Equal(t, "deploy", cmd.Use) @@ -218,7 +218,7 @@ func TestNewOperationDeployModelCommand(t *testing.T) { } func TestNewOperationDeployModelCommand_Flags(t *testing.T) { - cmd := newOperationDeployModelCommand() + cmd := newOperationDeployModelCommand(&jobsFlags{}) expectedFlags := []struct { name string @@ -254,7 +254,7 @@ func TestNewOperationDeployModelCommand_Flags(t *testing.T) { } func TestNewOperationDeployModelCommand_RequiredFlags(t *testing.T) { - cmd := newOperationDeployModelCommand() + cmd := newOperationDeployModelCommand(&jobsFlags{}) // Check that job-id and deployment-name are marked as required jobIDFlag := cmd.Flags().Lookup("job-id") @@ -274,13 +274,13 @@ func TestCommandsHaveDescriptions(t *testing.T) { name string cmdFunc func() *cobra.Command }{ - {"submit", func() *cobra.Command { return newOperationSubmitCommand() }}, - {"show", func() *cobra.Command { return newOperationShowCommand() }}, - {"list", func() *cobra.Command { return newOperationListCommand() }}, - {"pause", func() *cobra.Command { return newOperationPauseCommand() }}, - {"resume", func() *cobra.Command { return newOperationResumeCommand() }}, - {"cancel", func() *cobra.Command { return newOperationCancelCommand() }}, - {"deploy", func() *cobra.Command { return newOperationDeployModelCommand() }}, + {"submit", func() *cobra.Command { return newOperationSubmitCommand(&jobsFlags{}) }}, + {"show", func() *cobra.Command { return newOperationShowCommand(&jobsFlags{}) }}, + {"list", func() *cobra.Command { return newOperationListCommand(&jobsFlags{}) }}, + {"pause", func() *cobra.Command { return newOperationPauseCommand(&jobsFlags{}) }}, + {"resume", func() *cobra.Command { return newOperationResumeCommand(&jobsFlags{}) }}, + {"cancel", func() *cobra.Command { return newOperationCancelCommand(&jobsFlags{}) }}, + {"deploy", func() *cobra.Command { return newOperationDeployModelCommand(&jobsFlags{}) }}, } for _, tc := range commands { diff --git a/cli/azd/extensions/azure.ai.finetune/internal/cmd/validation.go b/cli/azd/extensions/azure.ai.finetune/internal/cmd/validation.go index db0fd4677be..eb36badc29f 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/cmd/validation.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/cmd/validation.go @@ -6,7 +6,6 @@ package cmd import ( "context" "fmt" - "regexp" "sort" "strings" @@ -16,36 +15,6 @@ import ( "azure.ai.finetune/internal/utils" ) -// sanitizeEnvironmentName converts a project name to a valid azd environment name. -// azd environment names must contain only lowercase letters, numbers, and hyphens, -// and must start and end with a letter or number. -func sanitizeEnvironmentName(name string) string { - // Convert to lowercase - result := strings.ToLower(name) - - // Replace spaces, underscores, and other common separators with hyphens - result = strings.ReplaceAll(result, " ", "-") - result = strings.ReplaceAll(result, "_", "-") - - // Remove any characters that aren't lowercase letters, numbers, or hyphens - re := regexp.MustCompile(`[^a-z0-9-]`) - result = re.ReplaceAllString(result, "") - - // Replace multiple consecutive hyphens with a single hyphen - re = regexp.MustCompile(`-+`) - result = re.ReplaceAllString(result, "-") - - // Trim leading and trailing hyphens (must start/end with letter or number) - result = strings.Trim(result, "-") - - // If empty after sanitization, use a default name - if result == "" { - result = "finetuning-env" - } - - return result -} - // Common hints for required flags const ( HintFindJobID = "To find job IDs, run: azd ai finetuning jobs list" @@ -138,9 +107,23 @@ func validateSubmitFlags(file, model, trainingFile string) error { // validateOrInitEnvironment checks if environment is configured, and if not, attempts implicit initialization // using the provided subscription ID and project endpoint flags. +// Priority order: (1) flags, (2) azd environment variables, (3) error with guidance. func validateOrInitEnvironment(ctx context.Context, subscriptionId, projectEndpoint string) error { ctx = azdext.WithAccessToken(ctx) + // Priority 1: If explicit flags are provided, validate their format and use them directly. + // This takes precedence over any previously configured azd environment. + if projectEndpoint != "" { + if subscriptionId == "" { + return fmt.Errorf("--subscription (-s) is required when --project-endpoint (-e) is provided") + } + if _, _, err := parseProjectEndpoint(projectEndpoint); err != nil { + return fmt.Errorf("invalid --project-endpoint: %w", err) + } + return nil + } + + // Priority 2: Check whether the azd environment already has all required variables. azdClient, err := azdext.NewAzdClient() if err != nil { return err @@ -150,7 +133,6 @@ func validateOrInitEnvironment(ctx context.Context, subscriptionId, projectEndpo envValues, _ := utils.GetEnvironmentValues(ctx, azdClient) required := []string{utils.EnvAzureTenantID, utils.EnvAzureSubscriptionID, utils.EnvAzureLocation, utils.EnvAzureAccountName} - // Check if environment is already configured allConfigured := true for _, varName := range required { if envValues[varName] == "" { @@ -160,50 +142,9 @@ func validateOrInitEnvironment(ctx context.Context, subscriptionId, projectEndpo } if allConfigured { - // Warn user if they provided flags that will be ignored - if subscriptionId != "" || projectEndpoint != "" { - color.Yellow("Warning: Environment is already configured. The --subscription and --project-endpoint flags are being ignored.") - color.Yellow("To reconfigure, run 'azd ai finetuning init' with the new values.\n") - } return nil } - // Environment not configured - check if we have flags for implicit init - if projectEndpoint == "" || subscriptionId == "" { - return fmt.Errorf("required environment variables not set. Either run 'azd ai finetuning init' or provide both --subscription (-s) and --project-endpoint (-e) flags") - } - - // Perform implicit initialization - fmt.Println("Environment not configured. Running implicit initialization...") - - // Extract project name from endpoint to use as default environment name - _, projectName, err := parseProjectEndpoint(projectEndpoint) - if err != nil { - return fmt.Errorf("failed to parse project endpoint: %w", err) - } - - // Sanitize project name for use as azd environment name - // (must be lowercase letters, numbers, hyphens, and start/end with letter or number) - envName := sanitizeEnvironmentName(projectName) - - initFlags := &initFlags{ - subscriptionId: subscriptionId, - projectEndpoint: projectEndpoint, - env: envName, - } - initFlags.NoPrompt = true // Run in non-interactive mode - - // Ensure project exists first (required before creating environment) - _, err = ensureProject(ctx, initFlags, azdClient) - if err != nil { - return fmt.Errorf("implicit initialization failed: %w", err) - } - - _, err = ensureEnvironment(ctx, initFlags, azdClient) - if err != nil { - return fmt.Errorf("implicit initialization failed: %w", err) - } - - fmt.Println("Environment configured successfully.") - return nil + // Priority 3: Neither flags nor environment are configured. + return fmt.Errorf("required environment variables not set. Either run 'azd ai finetuning init' or provide both --subscription (-s) and --project-endpoint (-e) flags") } diff --git a/cli/azd/extensions/azure.ai.finetune/internal/providers/factory/provider_factory.go b/cli/azd/extensions/azure.ai.finetune/internal/providers/factory/provider_factory.go index b531355250a..92a89ccfa20 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/providers/factory/provider_factory.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/providers/factory/provider_factory.go @@ -123,6 +123,26 @@ func NewFineTuningProvider(ctx context.Context, azdClient *azdext.AzdClient) (pr return openaiprovider.NewOpenAIProvider(client), err } +// NewFineTuningProviderWithEndpoint creates a FineTuningProvider using a direct project endpoint URL +// and tenant ID, bypassing azd environment configuration. This allows callers to supply the +// endpoint and credentials explicitly rather than relying on stored environment values. +func NewFineTuningProviderWithEndpoint(endpoint, tenantId string) (providers.FineTuningProvider, error) { + credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{ + TenantID: tenantId, + AdditionallyAllowedTenants: []string{"*"}, + }) + if err != nil { + return nil, fmt.Errorf("failed to create azure credential: %w", err) + } + + client := openai.NewClient( + option.WithBaseURL(endpoint), + option.WithQuery("api-version", DefaultApiVersion), + WithTokenCredential(credential, DefaultAzureFinetuningScope), + ) + return openaiprovider.NewOpenAIProvider(&client), nil +} + // NewModelDeploymentProvider creates a ModelDeploymentProvider based on provider type func NewModelDeploymentProvider(subscriptionId string, credential azcore.TokenCredential) (providers.ModelDeploymentProvider, error) { clientFactory, err := armcognitiveservices.NewClientFactory( diff --git a/cli/azd/extensions/azure.ai.finetune/internal/services/finetune_service.go b/cli/azd/extensions/azure.ai.finetune/internal/services/finetune_service.go index ae836daa32e..13e188a7b3a 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/services/finetune_service.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/services/finetune_service.go @@ -40,6 +40,16 @@ func NewFineTuningService(ctx context.Context, azdClient *azdext.AzdClient, stat }, nil } +// NewFineTuningServiceWithProvider creates a FineTuningService with a pre-built provider. +// Use this when the provider has been constructed directly (e.g., from explicit flag values) +// rather than from azd environment configuration. +func NewFineTuningServiceWithProvider(provider providers.FineTuningProvider, stateStore StateStore) FineTuningService { + return &fineTuningServiceImpl{ + provider: provider, + stateStore: stateStore, + } +} + // CreateFineTuningJob creates a new fine-tuning job with business validation func (s *fineTuningServiceImpl) CreateFineTuningJob(ctx context.Context, req *models.CreateFineTuningRequest) (*models.FineTuningJob, error) { // Validate request diff --git a/cli/azd/extensions/azure.ai.finetune/version.txt b/cli/azd/extensions/azure.ai.finetune/version.txt index 4a615f30a4e..575667c628c 100644 --- a/cli/azd/extensions/azure.ai.finetune/version.txt +++ b/cli/azd/extensions/azure.ai.finetune/version.txt @@ -1 +1 @@ -0.0.17-preview +0.0.18-preview