Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 9 additions & 15 deletions src/aleph_client/commands/instance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import shutil
from decimal import Decimal
from pathlib import Path
from typing import Annotated, Any, Optional, Union
from typing import Annotated, Any, Optional

import aiohttp
import typer
Expand All @@ -30,8 +30,6 @@
)
from aleph.sdk.query.responses import PriceResponse
from aleph.sdk.types import (
CrnExecutionV1,
CrnExecutionV2,
InstanceAllocationsInfo,
InstanceWithScheduler,
PortFlags,
Expand Down Expand Up @@ -213,7 +211,7 @@ async def create(
)
)
async with AuthenticatedAlephHttpClient(account=account) as client:
vouchers = await client.voucher.fetch_vouchers_by_chain(chain=Chain(account.CHAIN))
vouchers = await client.voucher.fetch_vouchers_by_chain(address=address, chain=Chain(account.CHAIN))
if len(vouchers) == 0:
console.print("No NFT vouchers find on this account")
raise typer.Exit(code=1)
Expand Down Expand Up @@ -354,7 +352,9 @@ async def create(
)

if not tier:
pricing.display_table_for(entity=pricing_entity, network_gpu=found_gpu_models, tier=None)
pricing.display_table_for(
entity=pricing_entity, network_gpu=found_gpu_models, tier=None, payment_type=payment_type
)
tiers = list(pricing.data[pricing_entity].tiers)

# GPU entities: filter to tiers that actually use the selected GPUs
Expand Down Expand Up @@ -418,7 +418,8 @@ async def create(
compute_unit_price = pricing.data[pricing_entity].price.get("compute_unit")
if payment_type in [PaymentType.hold, PaymentType.superfluid]:
# Early check with minimal cost (Gas + Aleph ERC20)
balance_response = await client.get_balances(address)
async with AlephHttpClient(api_server=settings.API_HOST) as client:
balance_response = await client.get_balances(address)
available_amount = balance_response.balance - balance_response.locked_amount
available_funds = Decimal(0 if is_stream else available_amount)
try:
Expand Down Expand Up @@ -467,7 +468,6 @@ async def create(
echo(f"Failed to fetch credit info, error: {e}")
raise typer.Exit(code=1) from e

stream_reward_address = None
crn, crn_info, gpu_id = None, None, None
if is_stream or confidential or gpu or is_credit:
if crn_url:
Expand Down Expand Up @@ -607,7 +607,7 @@ async def create(

payment = Payment(
chain=payment_chain,
receiver=stream_reward_address if stream_reward_address else None,
receiver=crn_info.stream_reward_address if crn_info and crn_info.stream_reward_address else None,
type=payment_type,
)

Expand All @@ -633,7 +633,7 @@ async def create(
try:
content = make_instance_content(**content_dict)
price: PriceResponse = await client.get_estimated_price(content)
required_tokens = Decimal(price.required_tokens)
required_tokens = price.required_tokens if price.cost is None else Decimal(price.cost)
except Exception as e:
echo(f"Failed to estimate instance cost, error: {e}")
raise typer.Exit(code=1) from e
Expand Down Expand Up @@ -836,7 +836,6 @@ async def delete(
existing_message: InstanceMessage = await client.get_message(
item_hash=ItemHash(item_hash), message_type=InstanceMessage
)

except MessageNotFoundError:
echo("Instance does not exist")
raise typer.Exit(code=1) from None
Expand Down Expand Up @@ -869,11 +868,6 @@ async def delete(
if isinstance(instance_info, InstanceWithScheduler):
echo(f"Instance {item_hash} was auto-scheduled, VM will be erased automatically.")
elif instance_info is not None and hasattr(instance_info, "crn_url") and instance_info.crn_url:
execution: Optional[Union[CrnExecutionV1, CrnExecutionV2]] = await client.crn.get_vm(
instance_info.crn_url, item_hash=ItemHash(item_hash)
)
if not execution:
echo("VM is not running or CRN not accessible, Skipping ...")
try:
async with VmClient(account, instance_info.crn_url) as manager:
status, _ = await manager.erase_instance(vm_id=item_hash)
Expand Down
2 changes: 1 addition & 1 deletion src/aleph_client/commands/instance/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ async def _prepare_instance_column(self):
# Price information
async with AlephHttpClient() as client:
price: PriceResponse = await client.get_program_price(self.message.item_hash)
required_tokens = Decimal(price.required_tokens)
required_tokens = price.required_tokens if price.cost is None else Decimal(price.cost)

if self.is_hold:
aleph_price = Text(f"{displayable_amount(required_tokens, decimals=3)} (fixed)", style="magenta3")
Expand Down
2 changes: 2 additions & 0 deletions src/aleph_client/commands/instance/port_forwarder.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ async def list_ports(
ports_map = config.root

for ih, ports in ports_map.items():
if not ports:
continue
name = await client.instance.get_name_of_executable(item_hash=ItemHash(ih))

# If an item hash is specified, only show that one
Expand Down
129 changes: 92 additions & 37 deletions src/aleph_client/commands/pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from aleph.sdk.conf import settings
from aleph.sdk.utils import displayable_amount
from aleph_message.models.execution.base import PaymentType
from rich import box
from rich.console import Console, Group
from rich.panel import Panel
Expand Down Expand Up @@ -105,9 +106,11 @@ def fmt(value, unit):
lines = [f"{prefix}$ALEPH (Holding): [bright_cyan]{holding}"]

# Show credits ONLY for storage, and only if a credit price exists
if storage and storage_price.credit:
credit = fmt(storage_price.credit, "credit")
lines.append(f"Credits: [bright_cyan]{credit}")
# Credits not handled for now on storage so we keep that comment for now

# if storage and storage_price.credit:
# credit = fmt(storage_price.credit, "credit")
# lines.append(f"Credits: [bright_cyan]{credit}")

infos.append(Text.from_markup("\n".join(lines)))

Expand All @@ -117,6 +120,7 @@ def build_column(
self,
entity: PricingEntity,
entity_info: PricingPerEntity,
payment_type: Optional[PaymentType] = None,
):
# Common Column for PROGRAM / INSTANCE / CONF / GPU
self.table.add_column("Tier", style="cyan")
Expand All @@ -131,13 +135,20 @@ def build_column(
self.table.add_column("VRAM (GiB)", style="orange1")

cu_price = entity_info.price.get("compute_unit")
if isinstance(cu_price, Price) and cu_price.holding:

if (isinstance(cu_price, Price) and cu_price.holding) and (
payment_type is None or payment_type == PaymentType.hold
):
self.table.add_column("$ALEPH (Holding)", style="red", justify="center")

if isinstance(cu_price, Price) and cu_price.payg and entity in PAYG_GROUP:
if (isinstance(cu_price, Price) and cu_price.payg and entity in PAYG_GROUP) and (
payment_type is None or payment_type == PaymentType.superfluid
):
self.table.add_column("$ALEPH (Pay-As-You-Go)", style="green", justify="center")

if isinstance(cu_price, Price) and cu_price.credit:
if (isinstance(cu_price, Price) and cu_price.credit) and (
payment_type is None or payment_type == PaymentType.credit
):
self.table.add_column("$ Credits", style="green", justify="center")

if entity in PRICING_GROUPS[GroupEntity.PROGRAM]:
Expand All @@ -149,6 +160,7 @@ def fill_tier(
entity: PricingEntity,
entity_info: PricingPerEntity,
network_gpu: Optional[NetworkGPUS] = None,
payment_type: Optional[PaymentType] = None,
):
tier_id = self._format_tier_id(tier.id)
self.table.add_section()
Expand Down Expand Up @@ -190,7 +202,9 @@ def fill_tier(

cu_price = entity_info.price.get("compute_unit")
# Fill Holding row
if isinstance(cu_price, Price) and cu_price.holding:
if (isinstance(cu_price, Price) and cu_price.holding) and (
payment_type is None or payment_type == PaymentType.hold
):
if entity == PricingEntity.INSTANCE_CONFIDENTIAL or (
entity == PricingEntity.INSTANCE and tier.compute_units > 4
):
Expand All @@ -200,15 +214,19 @@ def fill_tier(
f"{displayable_amount(Decimal(str(cu_price.holding)) * tier.compute_units, decimals=3)} tokens"
)
# Fill PAYG row
if isinstance(cu_price, Price) and cu_price.payg and entity in PAYG_GROUP:
if (isinstance(cu_price, Price) and cu_price.payg and entity in PAYG_GROUP) and (
payment_type is None or payment_type == PaymentType.superfluid
):
payg_price = cu_price.payg
payg_hourly = Decimal(str(payg_price)) * tier.compute_units
row.append(
f"{displayable_amount(payg_hourly, decimals=3)} token/hour"
f"\n{displayable_amount(payg_hourly * 24, decimals=3)} token/day"
)
# Fill Credit row
if isinstance(cu_price, Price) and cu_price.credit:
if (isinstance(cu_price, Price) and cu_price.credit) and (
payment_type is None or payment_type == PaymentType.credit
):
credit_price = cu_price.credit
credit_hourly = Decimal(str(credit_price)) * tier.compute_units
row.append(
Expand Down Expand Up @@ -237,6 +255,7 @@ def fill_column(
entity_info: PricingPerEntity,
network_gpu: Optional[NetworkGPUS],
only_tier: Optional[int] = None, # <-- now int
payment_type: Optional[PaymentType] = None,
):
any_added = False

Expand All @@ -247,12 +266,18 @@ def fill_column(
for tier in entity_info.tiers:
if not self._tier_matches(tier, only_tier):
continue
self.fill_tier(tier=tier, entity=entity, entity_info=entity_info, network_gpu=network_gpu)
self.fill_tier(
tier=tier, entity=entity, entity_info=entity_info, network_gpu=network_gpu, payment_type=payment_type
)
any_added = True
return any_added

def display_table_for(
self, entity: PricingEntity, network_gpu: Optional[NetworkGPUS] = None, tier: Optional[int] = None
self,
entity: PricingEntity,
network_gpu: Optional[NetworkGPUS] = None,
tier: Optional[int] = None,
payment_type: Optional[PaymentType] = None,
):
info = self.data[entity]
label = self._format_name(entity=entity)
Expand All @@ -279,39 +304,68 @@ def display_table_for(
)
self.table = table

self.build_column(entity=entity, entity_info=info)
self.build_column(entity=entity, entity_info=info, payment_type=payment_type)

any_rows_added = self.fill_column(entity=entity, entity_info=info, network_gpu=network_gpu, only_tier=tier)
any_rows_added = self.fill_column(
entity=entity, entity_info=info, network_gpu=network_gpu, only_tier=tier, payment_type=payment_type
)

# If no rows were added, the filter was too restrictive
# So add all tiers without filter
if not any_rows_added:
self.fill_column(entity=entity, entity_info=info, network_gpu=network_gpu, only_tier=None)

storage_price = info.price.get("storage")
extra_price_holding = ""
if isinstance(storage_price, Price) and storage_price.holding:
extra_price_holding = (
f"[red]{displayable_amount(Decimal(str(storage_price.holding)) * 1024, decimals=5)}"
" token/GiB[/red] (Holding) -or- "
)

payg_storage_price = "0"
if isinstance(storage_price, Price) and storage_price.payg:
payg_storage_price = displayable_amount(Decimal(str(storage_price.payg)) * 1024 * 24, decimals=5)

extra_price_credits = "0"
if isinstance(storage_price, Price) and storage_price.credit:
extra_price_credits = displayable_amount(Decimal(str(storage_price.credit)) * 1024 * 24, decimals=5)
def build_extra_volume_cost(
storage_price: Optional[Price], payment_type: Optional[PaymentType] = None
) -> Text:
"""
Returns a Text markup with the correct extra volume cost
depending on payment_type.
"""

extra_price_holding = ""
payg_storage_price = "0"
extra_price_credits = "0"

if isinstance(storage_price, Price):
if storage_price.holding:
extra_price_holding = (
f"[red]{displayable_amount(Decimal(str(storage_price.holding)) * 1024, decimals=5)}"
" token/GiB[/red] (Holding)"
)

if storage_price.payg:
payg_storage_price = displayable_amount(
Decimal(str(storage_price.payg)) * 1024 * 24, decimals=5
)

if storage_price.credit:
extra_price_credits = displayable_amount(
Decimal(str(storage_price.credit)) * 1024 * 24, decimals=5
)

# Handle by payment_type
if payment_type is None:
return Text.from_markup(
f"Extra Volume Cost: {extra_price_holding} "
f"[green]{payg_storage_price} token/GiB/day[/green] (Pay-As-You-Go) "
f"-or- [green]{extra_price_credits} credit/GiB/day[/green] (Credits)\n"
)
elif payment_type == PaymentType.superfluid:
return Text.from_markup(
f"Extra Volume Cost: [green]{payg_storage_price} token/GiB/day[/green] (Pay-As-You-Go)\n"
)
elif payment_type == PaymentType.hold:
return Text.from_markup(f"Extra Volume Cost: {extra_price_holding}\n")
elif payment_type == PaymentType.credit:
return Text.from_markup(
f"Extra Volume Cost: [green]{extra_price_credits} credit/GiB/day[/green] (Credits)\n"
)
else:
return Text("Extra Volume Cost: Unknown payment type\n")

infos = [
Text.from_markup(
f"Extra Volume Cost: {extra_price_holding}"
f"[green]{payg_storage_price}"
" token/GiB/day[/green] (Pay-As-You-Go)"
f" -or- [green]{extra_price_credits} credit/GiB/day[/green] (Credits)\n"
)
]
storage_price = info.price.get("storage")
infos = [build_extra_volume_cost(storage_price=storage_price, payment_type=payment_type)]
displayable_group = Group(
self.table,
Text.assemble(*infos),
Expand Down Expand Up @@ -340,6 +394,7 @@ async def fetch_pricing_aggregate() -> Pricing:
async def prices_for_service(
service: Annotated[GroupEntity, typer.Argument(help="Service to display pricing for")],
tier: Annotated[Optional[int], typer.Option(help="Service specific Tier")] = None,
payment_type: Annotated[Optional[PaymentType], typer.Option(help="Payment Type")] = None,
json: Annotated[bool, typer.Option(help="JSON output instead of Rich Table")] = False,
no_null: Annotated[bool, typer.Option(help="Exclude null values in JSON output")] = False,
with_current_availability: Annotated[
Expand Down Expand Up @@ -376,4 +431,4 @@ async def prices_for_service(
)
else:
for entity in group:
pricing.display_table_for(entity, network_gpu=network_gpu, tier=tier)
pricing.display_table_for(entity, network_gpu=network_gpu, tier=tier, payment_type=payment_type)
4 changes: 2 additions & 2 deletions src/aleph_client/commands/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ async def upload(
try:
content = make_program_content(**content_dict)
price: PriceResponse = await client.get_estimated_price(content)
required_tokens = Decimal(price.required_tokens)
required_tokens = price.required_tokens if price.cost is None else Decimal(price.cost)
except Exception as e:
typer.echo(f"Failed to estimate program cost, error: {e}")
raise typer.Exit(code=1) from e
Expand Down Expand Up @@ -575,7 +575,7 @@ async def list_programs(
if message.sender != message.content.address:
payer = Text.assemble("\nPayer: ", Text(str(message.content.address), style="orange1"))
price: PriceResponse = await client.get_program_price(message.item_hash)
required_tokens = Decimal(price.required_tokens)
required_tokens = price.required_tokens if price.cost is None else Decimal(price.cost)
if price.payment_type == PaymentType.hold.value:
aleph_price = Text(
f"{displayable_amount(required_tokens, decimals=3)} (fixed)".ljust(13), style="violet"
Expand Down
14 changes: 10 additions & 4 deletions src/aleph_client/commands/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,16 @@ def get_annotated_constraint(annotated_type: type, constraint_name: str) -> Any
if not args:
return None

for arg in args:
if isinstance(arg, FieldInfo):
value = getattr(arg, constraint_name, None)
return value
# We only care about FieldInfo, not the base type (like int)
field_info = next((a for a in args if isinstance(a, FieldInfo)), None)
if not field_info:
return None

# FieldInfo stores constraint objects in its metadata list
for meta in getattr(field_info, "metadata", []):
if hasattr(meta, constraint_name):
return getattr(meta, constraint_name)

return None


Expand Down
Loading
Loading