Skip to content

{aimanager} Always show Feasible column in calculate-cost table output - #10357

Merged
Liqian Luo (circy9) merged 3 commits into
Azure:mainfrom
circy9:aimanager-feasible-column
Sep 18, 2026
Merged

Liqian Luo (circy9) merged 3 commits into
Azure:mainfrom
circy9:aimanager-feasible-column

Conversation

@circy9

@circy9 Liqian Luo (circy9) commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Validation — ️✔️ All clear

Breaking Changes
️✔️ None

Summary

az aimanager model calculate-cost -o table dropped the Feasible column whenever every SKU was infeasible — which is exactly the case where that column is most useful. The service omits feasible on infeasible plans, so the JMESPath projection rendered those cells blank, and azure-cli drops all-blank columns. The result was an unlabeled table with no way to tell that nothing was deployable.

  • Replace the CALCULATE_COST_TABLE_TRANSFORMER JMESPath string with a callable formatter (calculate_cost_table_format in _format.py) that coerces feasible to an explicit True/False for every plan, so the column is always populated and never dropped.
  • Add an InfeasibilityReason column surfacing the per-plan reason code (e.g. InsufficientQuota) for infeasible SKUs.
  • Bump extension version to 1.5.5b1 and add a HISTORY.rst entry.

Fixes AB#39624918

Test plan

  • azdev style aimanager — PASSED (pylint + flake8)
  • azdev linter aimanager — no violations
  • python -m unittest azext_aimanager.tests.latest.test_aimanager_format — 20 tests pass, including new TestCalculateCostTableFormat covering the all-infeasible regression, explicit True/False rendering, infeasibility reason, and empty/missing plans.

The 'az aimanager model calculate-cost -o table' output dropped the
'Feasible' column whenever every SKU was infeasible, because the service
omits 'feasible' on infeasible plans and azure-cli drops all-blank
columns — hiding the single most useful column exactly when it matters.

Replace the JMESPath table transformer with a callable formatter that
coerces 'feasible' to an explicit True/False for every plan, so the
column is always populated and never dropped. Also add an
'InfeasibilityReason' column surfacing the per-plan reason code.

Fixes AB#39624918
Copilot AI lite review requested due to automatic review settings September 17, 2026 22:09
@azure-client-tools-bot-prd

Copy link
Copy Markdown

Hi Liqian Luo (@circy9),
Please write the description of changes which can be perceived by customers into HISTORY.rst.
If you want to release a new extension version, please update the version in pyproject.toml (or setup.py, if the extension has not migrated yet) as well.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

All reviewed changes are covered by the supplied tests and no unresolved issues were identified.

Pull request overview

Updates calculate-cost -o table to always show feasibility and infeasibility reasons for model plans.

Changes:

  • Replaces the JMESPath transformer with a callable formatter.
  • Displays explicit True/False feasibility values.
  • Adds InfeasibilityReason output and regression tests.
  • Bumps the extension version to 1.5.5b1.
File summaries
File Description
src/aimanager/setup.py Updates the extension version.
src/aimanager/HISTORY.rst Documents the release changes.
src/aimanager/azext_aimanager/tests/latest/test_aimanager_format.py Adds formatter regression tests.
src/aimanager/azext_aimanager/constants.py Removes the obsolete transformer.
src/aimanager/azext_aimanager/commands.py Registers the callable formatter.
src/aimanager/azext_aimanager/_format.py Implements feasibility and reason formatting.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@circy9

Copy link
Copy Markdown
Contributor Author

Local test results ✅

Tested with azdev in a Python 3.13 venv against this branch.

azdev test aimanager --series

============================= 86 passed in 43.10s ==============================

All 86 unit + playback scenario tests pass, including the new TestCalculateCostTableFormat (5 cases) covering the all-infeasible regression, explicit True/False rendering, infeasibility reason surfacing, and empty/missing plans.

azdev style aimanager / azdev linter aimanager

Pylint: PASSED
Flake8: PASSED
No violations found for linter rules.

Live table render (the bug scenario)

Rendered the reported all-SKUs-infeasible response through the actual knack table renderer. Before this change the Feasible column was dropped entirely; now it is always present:

Feasible    InfeasibilityReason    VmHourlyPrice    VmSize                   VmsPerReplica
----------  ---------------------  ---------------  -----------------------  ---------------
False       InsufficientQuota      0.526            Standard_NC4as_T4_v3     2
False       RegionUnavailable      3.2              Standard_NV36ads_A10_v5  1

Mixed feasibility still renders correctly:

Feasible    InfeasibilityReason    MaxAvailableReplicas    Quantization    TotalHourlyPrice    VmHourlyPrice    VmSize                    VmsPerReplica
----------  ---------------------  ----------------------  --------------  ------------------  ---------------  ------------------------  ---------------
True                               9                       fp16            3.673               3.673            Standard_NC24ads_A100_v4  1
False       InsufficientQuota                                                                  0.526            Standard_NC4as_T4_v3      2

(Column ordering above is alphabetized by the isolated format_table test helper; real az ... -o table preserves the formatter's declared column order.)

@circy9

Copy link
Copy Markdown
Contributor Author

Real output from centraluseuap

Ran the actual az aimanager model calculate-cost -o table against a live subscription in centraluseuap with this branch active, for a very small and a very large model. Both happen to be entirely infeasible (insufficient quota) — precisely the case that previously dropped the column. The Feasible column now renders (all False), and the new InfeasibilityReason column explains why per SKU. Real -o table also preserves the declared column order (confirming the earlier caveat).

Small model — google/gemma-4-E2B-it

VmSize                     Feasible    VmsPerReplica    VmHourlyPrice    TotalHourlyPrice    InfeasibilityReason
-------------------------  ----------  ---------------  ---------------  ------------------  --------------------------------
Standard_NV36ads_A10_v5    False       1                3.2              3.2                 InfeasibleCode_InsufficientQuota
Standard_NC24ads_A100_v4   False       1                4.15             4.15                InfeasibleCode_InsufficientQuota
Standard_NV72ads_A10_v5    False       1                6.52             6.52                InfeasibleCode_InsufficientQuota
Standard_NC48ads_A100_v4   False       1                8.301            8.301               InfeasibleCode_InsufficientQuota
Standard_NC40ads_H100_v5   False       1                8.585            8.585               InfeasibleCode_InsufficientQuota
Standard_NC96ads_A100_v4   False       1                16.602           16.602              InfeasibleCode_InsufficientQuota
Standard_NC80adis_H100_v5  False       1                17.17            17.17               InfeasibleCode_InsufficientQuota
Standard_ND96amsr_A100_v4  False       1                37.03            37.03               InfeasibleCode_InsufficientQuota
Standard_ND96isr_H100_v5   False       1                120.934          120.934             InfeasibleCode_InsufficientQuota

Large model — nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4

VmSize                     Feasible    VmsPerReplica    VmHourlyPrice    TotalHourlyPrice     InfeasibilityReason
-------------------------  ----------  ---------------  ---------------  -------------------  ------------------------------------
Standard_NC24ads_A100_v4   False       6                4.15             24.900000000000002   InfeasibleCode_InsufficientQuota
Standard_NC48ads_A100_v4   False       3                8.301            24.903               InfeasibleCode_InsufficientQuota
Standard_NC96ads_A100_v4   False       2                16.602           33.204               InfeasibleCode_InsufficientQuota
Standard_ND96amsr_A100_v4  False       1                37.03            37.03                InfeasibleCode_InsufficientQuota
Standard_NC40ads_H100_v5   False       5                8.585            42.925000000000004   InfeasibleCode_InsufficientQuota
Standard_NC80adis_H100_v5  False       3                17.17            51.510000000000005   InfeasibleCode_InsufficientQuota
Standard_NV36ads_A10_v5    False       20               3.2              64.0                 InfeasibleCode_InefficientDeployment
Standard_NV72ads_A10_v5    False       10               6.52             65.19999999999999    InfeasibleCode_InefficientDeployment
Standard_ND96isr_H100_v5   False       1                120.934          120.934              InfeasibleCode_InsufficientQuota

The larger model correctly requires more VmsPerReplica (6/3/2 A100s vs 1 for the small model) and surfaces a mix of InsufficientQuota and InefficientDeployment reasons. Before this fix, both tables would have shown neither a Feasible nor an InfeasibilityReason column — leaving the user no indication that nothing was deployable.

@circy9

Copy link
Copy Markdown
Contributor Author

Real output — mixed feasible/infeasible (sub with actual GPU quota) ✅

Re-ran in centraluseuap against a subscription that does have GPU quota (an A100 family available). This produces the more interesting mixed case: one feasible SKU alongside many infeasible ones. Confirms feasible rows render normally (with MaxAvailableReplicas) while the infeasible rows — which used to disappear — now stay visible with their reason.

Small model — google/gemma-4-E2B-it

VmSize                     Feasible    VmsPerReplica    VmHourlyPrice    TotalHourlyPrice    MaxAvailableReplicas    InfeasibilityReason
-------------------------  ----------  ---------------  ---------------  ------------------  ----------------------  --------------------------------
Standard_ND96amsr_A100_v4  True        1                37.03            37.03               1
Standard_NV36ads_A10_v5    False       1                3.2              3.2                                         InfeasibleCode_InsufficientQuota
Standard_NC24ads_A100_v4   False       1                4.15             4.15                                        InfeasibleCode_InsufficientQuota
Standard_NV72ads_A10_v5    False       1                6.52             6.52                                        InfeasibleCode_InsufficientQuota
Standard_NC48ads_A100_v4   False       1                8.301            8.301                                       InfeasibleCode_InsufficientQuota
Standard_NC40ads_H100_v5   False       1                8.585            8.585                                       InfeasibleCode_InsufficientQuota
Standard_NC96ads_A100_v4   False       1                16.602           16.602                                      InfeasibleCode_InsufficientQuota
Standard_NC80adis_H100_v5  False       1                17.17            17.17                                       InfeasibleCode_InsufficientQuota
Standard_ND96isr_H100_v5   False       1                120.934          120.934                                     InfeasibleCode_InsufficientQuota

Large model — nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4

VmSize                     Feasible    VmsPerReplica    VmHourlyPrice    TotalHourlyPrice    MaxAvailableReplicas    InfeasibilityReason
-------------------------  ----------  ---------------  ---------------  ------------------  ----------------------  ------------------------------------
Standard_ND96amsr_A100_v4  True        1                37.03            37.03               1
Standard_NC24ads_A100_v4   False       6                4.15             24.900000000000002                          InfeasibleCode_InsufficientQuota
Standard_NC48ads_A100_v4   False       3                8.301            24.903                                      InfeasibleCode_InsufficientQuota
Standard_NC96ads_A100_v4   False       2                16.602           33.204                                      InfeasibleCode_InsufficientQuota
Standard_NC40ads_H100_v5   False       5                8.585            42.925000000000004                          InfeasibleCode_InsufficientQuota
Standard_NC80adis_H100_v5  False       3                17.17            51.510000000000005                          InfeasibleCode_InsufficientQuota
Standard_NV36ads_A10_v5    False       20               3.2              64.0                                        InfeasibleCode_InefficientDeployment
Standard_NV72ads_A10_v5    False       10               6.52             65.19999999999999                           InfeasibleCode_InefficientDeployment
Standard_ND96isr_H100_v5   False       1                120.934          120.934                                     InfeasibleCode_InsufficientQuota

The feasible ND96amsr_A100_v4 row shows MaxAvailableReplicas=1 and no reason; the infeasible rows show False + a reason code (InsufficientQuota for most, InefficientDeployment for the small A10 SKUs on the 550B model). Prior to this fix, the infeasible rows contributed only blank cells, so an all-infeasible table dropped the Feasible/InfeasibilityReason columns entirely.

The service returns reason codes prefixed with "InfeasibleCode_" (e.g.
"InfeasibleCode_InefficientDeployment"), which is redundant and makes the
column unnecessarily wide in -o table output. Strip the prefix so the
column shows just "InefficientDeployment" / "InsufficientQuota".
@circy9

Copy link
Copy Markdown
Contributor Author

Follow-up: strip InfeasibleCode_ prefix from InfeasibilityReason

Per review feedback, the reason codes were too long (InfeasibleCode_InefficientDeployment). The formatter now strips the redundant InfeasibleCode_ prefix. Codes without the prefix pass through unchanged (new unit test added). Live centraluseuap output (large model, mixed feasibility):

VmSize                     Feasible    VmsPerReplica    VmHourlyPrice    TotalHourlyPrice    MaxAvailableReplicas    InfeasibilityReason
-------------------------  ----------  ---------------  ---------------  ------------------  ----------------------  ---------------------
Standard_ND96amsr_A100_v4  True        1                37.03            37.03               1
Standard_NC24ads_A100_v4   False       6                4.15             24.900000000000002                          InsufficientQuota
Standard_NC48ads_A100_v4   False       3                8.301            24.903                                      InsufficientQuota
Standard_NC96ads_A100_v4   False       2                16.602           33.204                                      InsufficientQuota
Standard_NC40ads_H100_v5   False       5                8.585            42.925000000000004                          InsufficientQuota
Standard_NC80adis_H100_v5  False       3                17.17            51.510000000000005                          InsufficientQuota
Standard_NV36ads_A10_v5    False       20               3.2              64.0                                        InefficientDeployment
Standard_NV72ads_A10_v5    False       10               6.52             65.19999999999999                           InefficientDeployment
Standard_ND96isr_H100_v5   False       1                120.934          120.934                                     InsufficientQuota

azdev style PASSED; test_aimanager_format now 21 tests pass.

@yonzhan

Copy link
Copy Markdown
Collaborator

aimanager

Comment thread src/aimanager/azext_aimanager/_format.py Outdated
Comment thread src/aimanager/azext_aimanager/_format.py
Address PR review feedback:
- Coerce 'feasible' via a strict helper so a JSON-string "false" becomes
  False (bool("false") is truthy in Python). The vendored SDK already
  deserializes it as a real bool, but this guards any raw-dict path.
- Mention the Feasible / InfeasibilityReason table columns in the
  calculate-cost command help long-summary.
@circy9
Liqian Luo (circy9) merged commit 0bfdca1 into Azure:main Sep 18, 2026
24 checks passed
@azclibot

Copy link
Copy Markdown
Collaborator

[Release] Update index.json for extension [ aimanager-1.5.5b1 ] : https://dev.azure.com/msazure/One/_build/results?buildId=181767151&view=results

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AKS Auto-Assign Auto assign by bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants