Skip to content
38 changes: 24 additions & 14 deletions cli/azd/extensions/azure.ai.finetune/internal/cmd/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/azure/azure-dev/cli/azd/pkg/ux"
"github.com/fatih/color"

"github.com/spf13/cobra"

FTYaml "azure.ai.finetune/internal/fine_tuning_yaml"
"azure.ai.finetune/internal/services"
JobWrapper "azure.ai.finetune/internal/tools"
)

func newOperationCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "jobs",
Use: "jobs",
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
return validateEnvironment(cmd.Context())
},
Short: "Manage fine-tuning jobs",
}

Expand Down Expand Up @@ -253,12 +256,13 @@ func newOperationShowCommand() *cobra.Command {
return cmd
}

// newOperationListCommand creates a command to list fine-tuning jobs
func newOperationListCommand() *cobra.Command {
var top int
var limit int
var after string
cmd := &cobra.Command{
Use: "list",
Short: "List the fine tuning jobs",
Short: "list the fine tuning jobs",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := azdext.WithAccessToken(cmd.Context())
azdClient, err := azdext.NewAzdClient()
Expand All @@ -269,32 +273,38 @@ func newOperationListCommand() *cobra.Command {

// Show spinner while fetching jobs
spinner := ux.NewSpinner(&ux.SpinnerOptions{
Text: "Fetching fine-tuning jobs...",
Text: "fetching fine-tuning jobs...",
})
if err := spinner.Start(ctx); err != nil {
fmt.Printf("Failed to start spinner: %v\n", err)
fmt.Printf("failed to start spinner: %v\n", err)
}

// List fine-tuning jobs using job wrapper
jobs, err := JobWrapper.ListJobs(ctx, azdClient, top, after)
_ = spinner.Stop(ctx)
fineTuneSvc, err := services.NewFineTuningService(ctx, azdClient, nil)
if err != nil {
_ = spinner.Stop(ctx)
fmt.Println()
return err
}

jobs, err := fineTuneSvc.ListFineTuningJobs(ctx, limit, after)
_ = spinner.Stop(ctx)
if err != nil {
return fmt.Errorf("failed to list fine-tuning jobs: %w", err)
fmt.Println()
return err
}

for i, job := range jobs {
fmt.Printf("\n%d. Job ID: %s | Status: %s %s | Model: %s | Fine-tuned: %s | Created: %s",
i+1, job.Id, getStatusSymbol(job.Status), job.Status, job.Model, formatFineTunedModel(job.FineTunedModel), job.CreatedAt)
i+1, job.ID, getStatusSymbol(string(job.Status)), job.Status, job.BaseModel, formatFineTunedModel(job.FineTunedModel), job.CreatedAt)
}

fmt.Printf("\nTotal jobs: %d\n", len(jobs))
fmt.Printf("\ntotal jobs: %d\n", len(jobs))

return nil
},
}
cmd.Flags().IntVarP(&top, "top", "t", 50, "Number of fine-tuning jobs to list")
cmd.Flags().StringVarP(&after, "after", "a", "", "Cursor for pagination")
cmd.Flags().IntVarP(&limit, "top", "t", 50, "number of fine-tuning jobs to list")
cmd.Flags().StringVarP(&after, "after", "a", "", "cursor for pagination")
return cmd
}

Expand Down
8 changes: 3 additions & 5 deletions cli/azd/extensions/azure.ai.finetune/internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,19 @@ func NewRootCommand() *cobra.Command {
"Enable debug mode",
)

// Adds support for `--no-prompt` global flag in azd
// Without this the extension command will error when the flag is provided
// Adds support for `--no-prompt` global flag in azd.
// Without this the extension command will error when the flag is provided.
rootCmd.PersistentFlags().BoolVar(
&rootFlags.NoPrompt,
"no-prompt",
false,
"Accepts the default value instead of prompting, or it fails if there is no default.",
"accepts the default value instead of prompting, or fails if there is no default",
)

rootCmd.AddCommand(newListenCommand())
rootCmd.AddCommand(newVersionCommand())
rootCmd.AddCommand(newInitCommand(rootFlags))
rootCmd.AddCommand(newOperationCommand())
// rootCmd.AddCommand(newOperationListCommand())
//rootCmd.AddCommand(newOperationCheckpointsCommand())

return rootCmd
}
32 changes: 32 additions & 0 deletions cli/azd/extensions/azure.ai.finetune/internal/cmd/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"fmt"

"azure.ai.finetune/internal/utils"
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
)

func validateEnvironment(ctx context.Context) error {
ctx = azdext.WithAccessToken(ctx)

azdClient, err := azdext.NewAzdClient()
if err != nil {
return err
}
defer azdClient.Close()

envValues, _ := utils.GetEnvironmentValues(ctx, azdClient)
required := []string{utils.EnvAzureTenantID, utils.EnvAzureSubscriptionID, utils.EnvAzureLocation, utils.EnvAzureAccountName}

for _, varName := range required {
if envValues[varName] == "" {
return fmt.Errorf("required environment variables not set. Please run 'azd ai finetune init' command to configure your environment")
}
}
return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,19 @@ package azure
import (
"context"

"azure.ai.finetune/internal/providers"
"azure.ai.finetune/pkg/models"
)

// Ensure AzureProvider implements FineTuningProvider and ModelDeploymentProvider interfaces
var (
_ providers.FineTuningProvider = (*AzureProvider)(nil)
_ providers.ModelDeploymentProvider = (*AzureProvider)(nil)
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices"
)

// AzureProvider implements the provider interface for Azure APIs
// This includes both Azure OpenAI and Azure Cognitive Services APIs
type AzureProvider struct {
// TODO: Add Azure SDK clients
// cognitiveServicesClient *armcognitiveservices.Client
endpoint string
apiKey string
clientFactory *armcognitiveservices.ClientFactory
}

// NewAzureProvider creates a new Azure provider instance
func NewAzureProvider(endpoint, apiKey string) *AzureProvider {
func NewAzureProvider(clientFactory *armcognitiveservices.ClientFactory) *AzureProvider {
return &AzureProvider{
endpoint: endpoint,
apiKey: apiKey,
clientFactory: clientFactory,
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package factory

import (
"context"
"fmt"

"azure.ai.finetune/internal/providers"
azureprovider "azure.ai.finetune/internal/providers/azure"
openaiprovider "azure.ai.finetune/internal/providers/openai"
"azure.ai.finetune/internal/utils"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices"
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/azure"
"github.com/openai/openai-go/v3/option"
)

const (
// OpenAI API version for Azure cognitive services
apiVersion = "2025-04-01-preview"
// Azure cognitive services endpoint URL pattern
azureCognitiveServicesEndpoint = "https://%s.cognitiveservices.azure.com/openai"
)

func GetOpenAIClientFromAzdClient(ctx context.Context, azdClient *azdext.AzdClient) (*openai.Client, error) {
envValueMap, err := utils.GetEnvironmentValues(ctx, azdClient)
if err != nil {
return nil, fmt.Errorf("failed to get environment values: %w", err)
}

azureContext := &azdext.AzureContext{
Scope: &azdext.AzureScope{
Comment thread
saanikaguptamicrosoft marked this conversation as resolved.
TenantId: envValueMap[utils.EnvAzureTenantID],
SubscriptionId: envValueMap[utils.EnvAzureSubscriptionID],
Location: envValueMap[utils.EnvAzureLocation],
},
Resources: []string{},
}

credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{
TenantID: azureContext.Scope.TenantId,
AdditionallyAllowedTenants: []string{"*"},
})
if err != nil {
return nil, fmt.Errorf("failed to create azure credential: %w", err)
}

// Get Azure credentials and endpoint - TODO
// You'll need to get these from your environment or config
accountName := envValueMap[utils.EnvAzureAccountName]
endpoint := fmt.Sprintf(azureCognitiveServicesEndpoint, accountName)
// Create OpenAI client
client := openai.NewClient(
//azure.WithEndpoint(endpoint, apiVersion),
option.WithBaseURL(endpoint),
option.WithQuery("api-version", apiVersion),
azure.WithTokenCredential(credential),
)
return &client, nil
}

// NewFineTuningProvider creates a FineTuningProvider based on provider type
func NewFineTuningProvider(ctx context.Context, azdClient *azdext.AzdClient) (providers.FineTuningProvider, error) {
client, err := GetOpenAIClientFromAzdClient(ctx, azdClient)
return openaiprovider.NewOpenAIProvider(client), err
}

// NewModelDeploymentProvider creates a ModelDeploymentProvider based on provider type
func NewModelDeploymentProvider(subscriptionId string, credential azcore.TokenCredential) (providers.ModelDeploymentProvider, error) {
clientFactory, err := armcognitiveservices.NewClientFactory(
subscriptionId,
credential,
nil,
)
if err != nil {
return nil, fmt.Errorf("failed to create armcognitiveservices client factory: %w", err)
}
return azureprovider.NewAzureProvider(clientFactory), err
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package openai

import (
"azure.ai.finetune/internal/utils"
"azure.ai.finetune/pkg/models"
"github.com/openai/openai-go/v3"
)

// OpenAI Status Constants - matches OpenAI SDK values
const (
OpenAIStatusValidatingFiles = "validating_files"
OpenAIStatusQueued = "queued"
OpenAIStatusRunning = "running"
OpenAIStatusSucceeded = "succeeded"
OpenAIStatusFailed = "failed"
OpenAIStatusCancelled = "cancelled"
)

// mapOpenAIStatusToJobStatus converts OpenAI SDK status to domain model JobStatus
func mapOpenAIStatusToJobStatus(openaiStatus openai.FineTuningJobStatus) models.JobStatus {
switch openaiStatus {
case OpenAIStatusValidatingFiles, OpenAIStatusRunning:
return models.StatusRunning
case OpenAIStatusQueued:
return models.StatusQueued
case OpenAIStatusSucceeded:
return models.StatusSucceeded
case OpenAIStatusFailed:
return models.StatusFailed
case OpenAIStatusCancelled:
return models.StatusCancelled
default:
return models.StatusPending // Default fallback
}
}

// convertOpenAIJobToModel converts OpenAI SDK job to domain model
func convertOpenAIJobToModel(openaiJob openai.FineTuningJob) *models.FineTuningJob {
return &models.FineTuningJob{
ID: openaiJob.ID,
Status: mapOpenAIStatusToJobStatus(openaiJob.Status),
BaseModel: openaiJob.Model,
FineTunedModel: openaiJob.FineTunedModel,
CreatedAt: utils.UnixTimestampToUTC(openaiJob.CreatedAt),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,19 @@ package openai
import (
"context"

"azure.ai.finetune/internal/providers"
"azure.ai.finetune/pkg/models"
)

// Ensure OpenAIProvider implements FineTuningProvider and ModelDeploymentProvider interfaces
var (
_ providers.FineTuningProvider = (*OpenAIProvider)(nil)
_ providers.ModelDeploymentProvider = (*OpenAIProvider)(nil)
"github.com/openai/openai-go/v3"
)

// OpenAIProvider implements the provider interface for OpenAI APIs
type OpenAIProvider struct {
// TODO: Add OpenAI SDK client
// client *openai.Client
apiKey string
endpoint string
client *openai.Client
}

// NewOpenAIProvider creates a new OpenAI provider instance
func NewOpenAIProvider(apiKey, endpoint string) *OpenAIProvider {
func NewOpenAIProvider(client *openai.Client) *OpenAIProvider {
return &OpenAIProvider{
apiKey: apiKey,
endpoint: endpoint,
client: client,
}
}

Expand All @@ -49,8 +39,22 @@ func (p *OpenAIProvider) GetFineTuningStatus(ctx context.Context, jobID string)

// ListFineTuningJobs lists all fine-tuning jobs
func (p *OpenAIProvider) ListFineTuningJobs(ctx context.Context, limit int, after string) ([]*models.FineTuningJob, error) {
// TODO: Implement
return nil, nil
jobList, err := p.client.FineTuning.Jobs.List(ctx, openai.FineTuningJobListParams{
Limit: openai.Int(int64(limit)), // optional pagination control
After: openai.String(after),
Comment thread
achauhan-scc marked this conversation as resolved.
})

if err != nil {
return nil, err
}

var jobs []*models.FineTuningJob

for _, job := range jobList.Data {
finetuningJob := convertOpenAIJobToModel(job)
jobs = append(jobs, finetuningJob)
}
return jobs, nil
}

// GetFineTuningJobDetails retrieves detailed information about a job
Expand Down
Loading