Skip to content

Commit a8a3e0e

Browse files
Add table, json and yaml display options for list and show commands as per specs for finetuning extension (#6500)
* Initial changes to add utils for output formatting - json, yaml, table * Align jobs list command with specs * Align job show command with specs
1 parent 656c451 commit a8a3e0e

File tree

7 files changed

+611
-131
lines changed

7 files changed

+611
-131
lines changed

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

Lines changed: 57 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,6 @@ func newOperationCommand() *cobra.Command {
3636
return cmd
3737
}
3838

39-
// formatFineTunedModel returns the model name or "NA" if blank
40-
func formatFineTunedModel(model string) string {
41-
if model == "" {
42-
return "NA"
43-
}
44-
return model
45-
}
46-
4739
func newOperationSubmitCommand() *cobra.Command {
4840
var filename string
4941
var model string
@@ -154,10 +146,12 @@ func newOperationSubmitCommand() *cobra.Command {
154146
// newOperationShowCommand creates a command to show the fine-tuning job details
155147
func newOperationShowCommand() *cobra.Command {
156148
var jobID string
149+
var logs bool
150+
var output string
157151

158152
cmd := &cobra.Command{
159153
Use: "show",
160-
Short: "Show fine-tuning job details.",
154+
Short: "Shows detailed information about a specific job.",
161155
RunE: func(cmd *cobra.Command, args []string) error {
162156
ctx := azdext.WithAccessToken(cmd.Context())
163157
azdClient, err := azdext.NewAzdClient()
@@ -168,7 +162,7 @@ func newOperationShowCommand() *cobra.Command {
168162

169163
// Show spinner while fetching job
170164
spinner := ux.NewSpinner(&ux.SpinnerOptions{
171-
Text: fmt.Sprintf("Fetching fine-tuning job %s...", jobID),
165+
Text: "Fine-Tuning Job Details",
172166
})
173167
if err := spinner.Start(ctx); err != nil {
174168
fmt.Printf("failed to start spinner: %v\n", err)
@@ -177,106 +171,73 @@ func newOperationShowCommand() *cobra.Command {
177171
fineTuneSvc, err := services.NewFineTuningService(ctx, azdClient, nil)
178172
if err != nil {
179173
_ = spinner.Stop(ctx)
180-
fmt.Println()
174+
fmt.Print("\n\n")
181175
return err
182176
}
183177

184178
job, err := fineTuneSvc.GetFineTuningJobDetails(ctx, jobID)
185179
_ = spinner.Stop(ctx)
180+
fmt.Print("\n\n")
186181
if err != nil {
187-
fmt.Println()
188182
return err
189183
}
190184

191-
// Display job details
192-
color.Green("\nFine-tuning Job Details\n")
193-
fmt.Printf("Job ID: %s\n", job.ID)
194-
fmt.Printf("Status: %s %s\n", utils.GetStatusSymbol(job.Status), job.Status)
195-
fmt.Printf("Model: %s\n", job.Model)
196-
fmt.Printf("Fine-tuned Model: %s\n", formatFineTunedModel(job.FineTunedModel))
197-
fmt.Printf("Created At: %s\n", utils.FormatTime(job.CreatedAt))
198-
if !job.FinishedAt.IsZero() {
199-
fmt.Printf("Finished At: %s\n", utils.FormatTime(job.FinishedAt))
200-
}
201-
fmt.Printf("Method: %s\n", job.Method)
202-
fmt.Printf("Training File: %s\n", job.TrainingFile)
203-
if job.ValidationFile != "" {
204-
fmt.Printf("Validation File: %s\n", job.ValidationFile)
185+
switch output {
186+
case "json":
187+
utils.PrintObject(job, utils.FormatJSON)
188+
case "yaml":
189+
utils.PrintObject(job, utils.FormatYAML)
190+
case "table", "":
191+
views := job.ToDetailViews()
192+
indent := " "
193+
utils.PrintObjectWithIndent(views.Details, utils.FormatTable, indent)
194+
195+
fmt.Println("\nTimestamps:")
196+
utils.PrintObjectWithIndent(views.Timestamps, utils.FormatTable, indent)
197+
fmt.Println("\nConfiguration:")
198+
utils.PrintObjectWithIndent(views.Configuration, utils.FormatTable, indent)
199+
200+
fmt.Println("\nData:")
201+
utils.PrintObjectWithIndent(views.Data, utils.FormatTable, indent)
202+
default:
203+
return fmt.Errorf("unsupported output format: %s (supported: table, json, yaml)", output)
205204
}
206205

207-
// Print hyperparameters if available
208-
if job.Hyperparameters != nil {
209-
fmt.Println("\nHyperparameters:")
210-
fmt.Printf(" Batch Size: %d\n", job.Hyperparameters.BatchSize)
211-
fmt.Printf(" Learning Rate Multiplier: %f\n", job.Hyperparameters.LearningRateMultiplier)
212-
fmt.Printf(" N Epochs: %d\n", job.Hyperparameters.NEpochs)
213-
}
214-
215-
// Fetch and print events
216-
eventsSpinner := ux.NewSpinner(&ux.SpinnerOptions{
217-
Text: "Fetching job events...",
218-
})
219-
if err := eventsSpinner.Start(ctx); err != nil {
220-
fmt.Printf("failed to start spinner: %v\n", err)
221-
}
222-
223-
events, err := fineTuneSvc.GetJobEvents(ctx, jobID)
224-
_ = eventsSpinner.Stop(ctx)
225-
226-
if err != nil {
206+
if logs {
227207
fmt.Println()
228-
return err
229-
} else if events != nil && len(events.Data) > 0 {
230-
fmt.Println("\nJob Events:")
231-
for i, event := range events.Data {
232-
fmt.Printf(" %d. Event ID: %s\n", i+1, event.ID)
233-
fmt.Printf(" [%s] %s - %s\n", event.Level, utils.FormatTime(event.CreatedAt), event.Message)
234-
}
235-
if events.HasMore {
236-
fmt.Println(" ... (more events available)")
237-
}
238-
}
239-
240-
// Fetch and print checkpoints if job is completed
241-
if job.Status == models.StatusSucceeded {
242-
checkpointsSpinner := ux.NewSpinner(&ux.SpinnerOptions{
243-
Text: "Fetching job checkpoints...",
208+
// Fetch and print events
209+
eventsSpinner := ux.NewSpinner(&ux.SpinnerOptions{
210+
Text: "Events:",
244211
})
245-
if err := checkpointsSpinner.Start(ctx); err != nil {
212+
if err := eventsSpinner.Start(ctx); err != nil {
246213
fmt.Printf("failed to start spinner: %v\n", err)
247214
}
248215

249-
checkpoints, err := fineTuneSvc.GetJobCheckpoints(ctx, jobID)
250-
_ = checkpointsSpinner.Stop(ctx)
216+
events, err := fineTuneSvc.GetJobEvents(ctx, jobID)
217+
_ = eventsSpinner.Stop(ctx)
218+
fmt.Println()
251219

252220
if err != nil {
253-
fmt.Println()
254221
return err
255-
} else if checkpoints != nil && len(checkpoints.Data) > 0 {
256-
fmt.Println("\nJob Checkpoints:")
257-
for i, checkpoint := range checkpoints.Data {
258-
fmt.Printf(" %d. Checkpoint ID: %s\n", i+1, checkpoint.ID)
259-
fmt.Printf(" Checkpoint Name: %s\n", checkpoint.FineTunedModelCheckpoint)
260-
fmt.Printf(" Created On: %s\n", utils.FormatTime(checkpoint.CreatedAt))
261-
fmt.Printf(" Step Number: %d\n", checkpoint.StepNumber)
262-
if checkpoint.Metrics != nil {
263-
fmt.Printf(" Full Validation Loss: %.6f\n", checkpoint.Metrics.FullValidLoss)
264-
}
222+
} else if events != nil && len(events.Data) > 0 {
223+
const eventIndent = " "
224+
for _, event := range events.Data {
225+
fmt.Printf("%s[%s] %s\n", eventIndent, utils.FormatTime(event.CreatedAt), event.Message)
265226
}
266-
if checkpoints.HasMore {
267-
fmt.Println(" ... (more checkpoints available)")
227+
if events.HasMore {
228+
fmt.Println(" ... (more events available)")
268229
}
269230
}
270231
}
271232

272-
fmt.Println(strings.Repeat("=", 120))
273-
274233
return nil
275234
},
276235
}
277236

278-
cmd.Flags().StringVarP(&jobID, "job-id", "i", "", "Fine-tuning job ID")
279-
cmd.MarkFlagRequired("job-id")
237+
cmd.Flags().StringVarP(&jobID, "id", "i", "", "Job ID")
238+
cmd.Flags().BoolVar(&logs, "logs", false, "Include recent training logs")
239+
cmd.Flags().StringVarP(&output, "output", "o", "table", "Output format: table, json, yaml")
240+
cmd.MarkFlagRequired("id")
280241

281242
return cmd
282243
}
@@ -285,6 +246,7 @@ func newOperationShowCommand() *cobra.Command {
285246
func newOperationListCommand() *cobra.Command {
286247
var limit int
287248
var after string
249+
var output string
288250
cmd := &cobra.Command{
289251
Use: "list",
290252
Short: "List fine-tuning jobs.",
@@ -298,7 +260,7 @@ func newOperationListCommand() *cobra.Command {
298260

299261
// Show spinner while fetching jobs
300262
spinner := ux.NewSpinner(&ux.SpinnerOptions{
301-
Text: "Fetching fine-tuning jobs...",
263+
Text: "Fine-Tuning Jobs",
302264
})
303265
if err := spinner.Start(ctx); err != nil {
304266
fmt.Printf("failed to start spinner: %v\n", err)
@@ -313,25 +275,26 @@ func newOperationListCommand() *cobra.Command {
313275

314276
jobs, err := fineTuneSvc.ListFineTuningJobs(ctx, limit, after)
315277
_ = spinner.Stop(ctx)
278+
fmt.Print("\n\n")
279+
316280
if err != nil {
317-
fmt.Println()
318281
return err
319282
}
320283

321-
// Display job list
322-
for i, job := range jobs {
323-
fmt.Printf("\n%d. Job ID: %s | Status: %s %s | Model: %s | Fine-tuned: %s | Created: %s",
324-
i+1, job.ID, utils.GetStatusSymbol(job.Status), job.Status, job.BaseModel,
325-
formatFineTunedModel(job.FineTunedModel), utils.FormatTime(job.CreatedAt))
284+
switch output {
285+
case "json":
286+
utils.PrintObject(jobs, utils.FormatJSON)
287+
case "table", "":
288+
utils.PrintObject(jobs, utils.FormatTable)
289+
default:
290+
return fmt.Errorf("unsupported output format: %s (supported: table, json)", output)
326291
}
327-
328-
fmt.Printf("\nTotal jobs: %d\n", len(jobs))
329-
330292
return nil
331293
},
332294
}
333295

334-
cmd.Flags().IntVarP(&limit, "top", "t", 50, "Number of fine-tuning jobs to list")
335-
cmd.Flags().StringVarP(&after, "after", "a", "", "Cursor for pagination")
296+
cmd.Flags().IntVarP(&limit, "top", "t", 10, "Number of jobs to return")
297+
cmd.Flags().StringVar(&after, "after", "", "Pagination cursor")
298+
cmd.Flags().StringVarP(&output, "output", "o", "table", "Output format: table, json")
336299
return cmd
337300
}

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package openai
66
import (
77
"encoding/json"
88
"strings"
9+
"time"
910

1011
"github.com/openai/openai-go/v3"
1112
"github.com/openai/openai-go/v3/packages/pagination"
@@ -50,6 +51,7 @@ func convertOpenAIJobToModel(openaiJob openai.FineTuningJob) *models.FineTuningJ
5051
BaseModel: openaiJob.Model,
5152
FineTunedModel: openaiJob.FineTunedModel,
5253
CreatedAt: utils.UnixTimestampToUTC(openaiJob.CreatedAt),
54+
Duration: models.Duration(utils.CalculateDuration(openaiJob.CreatedAt, openaiJob.FinishedAt)),
5355
}
5456
}
5557

@@ -76,21 +78,37 @@ func convertOpenAIJobToDetailModel(openaiJob *openai.FineTuningJob) *models.Fine
7678
if openaiJob.Method.Reinforcement.Hyperparameters.ReasoningEffort != "" {
7779
hyperparameters.ReasoningEffort = string(openaiJob.Method.Reinforcement.Hyperparameters.ReasoningEffort)
7880
}
79-
8081
} else {
8182
// Fallback to top-level hyperparameters (for backward compatibility)
8283
hyperparameters.BatchSize = openaiJob.Hyperparameters.BatchSize.OfInt
8384
hyperparameters.LearningRateMultiplier = openaiJob.Hyperparameters.LearningRateMultiplier.OfFloat
8485
hyperparameters.NEpochs = openaiJob.Hyperparameters.NEpochs.OfInt
8586
}
8687

88+
status := mapOpenAIStatusToJobStatus(openaiJob.Status)
89+
90+
// Only set FinishedAt for terminal states
91+
var finishedAt *time.Time
92+
if utils.IsTerminalStatus(status) && openaiJob.FinishedAt > 0 {
93+
t := utils.UnixTimestampToUTC(openaiJob.FinishedAt)
94+
finishedAt = &t
95+
}
96+
97+
// Only set EstimatedFinish for non-terminal states
98+
var estimatedFinish *time.Time
99+
if !utils.IsTerminalStatus(status) && openaiJob.EstimatedFinish > 0 {
100+
t := utils.UnixTimestampToUTC(openaiJob.EstimatedFinish)
101+
estimatedFinish = &t
102+
}
103+
87104
jobDetail := &models.FineTuningJobDetail{
88105
ID: openaiJob.ID,
89-
Status: mapOpenAIStatusToJobStatus(openaiJob.Status),
106+
Status: status,
90107
Model: openaiJob.Model,
91108
FineTunedModel: openaiJob.FineTunedModel,
92109
CreatedAt: utils.UnixTimestampToUTC(openaiJob.CreatedAt),
93-
FinishedAt: utils.UnixTimestampToUTC(openaiJob.FinishedAt),
110+
FinishedAt: finishedAt,
111+
EstimatedFinish: estimatedFinish,
94112
Method: openaiJob.Method.Type,
95113
TrainingFile: openaiJob.TrainingFile,
96114
ValidationFile: openaiJob.ValidationFile,

0 commit comments

Comments
 (0)