diff --git a/langfuse/api/__init__.py b/langfuse/api/__init__.py index 451ad991e..c958219d5 100644 --- a/langfuse/api/__init__.py +++ b/langfuse/api/__init__.py @@ -157,6 +157,10 @@ PaginatedSessions, PatchMediaBody, PlaceholderMessage, + PricingTier, + PricingTierCondition, + PricingTierInput, + PricingTierOperator, Project, ProjectDeletionResponse, Projects, @@ -403,6 +407,10 @@ "PaginatedSessions", "PatchMediaBody", "PlaceholderMessage", + "PricingTier", + "PricingTierCondition", + "PricingTierInput", + "PricingTierOperator", "Project", "ProjectDeletionResponse", "Projects", diff --git a/langfuse/api/resources/__init__.py b/langfuse/api/resources/__init__.py index b3a6cc31a..d91362a28 100644 --- a/langfuse/api/resources/__init__.py +++ b/langfuse/api/resources/__init__.py @@ -84,6 +84,10 @@ Observation, ObservationLevel, ObservationsView, + PricingTier, + PricingTierCondition, + PricingTierInput, + PricingTierOperator, Score, ScoreConfig, ScoreDataType, @@ -424,6 +428,10 @@ "PaginatedSessions", "PatchMediaBody", "PlaceholderMessage", + "PricingTier", + "PricingTierCondition", + "PricingTierInput", + "PricingTierOperator", "Project", "ProjectDeletionResponse", "Projects", diff --git a/langfuse/api/resources/commons/__init__.py b/langfuse/api/resources/commons/__init__.py index 6dfbecafe..77097ba49 100644 --- a/langfuse/api/resources/commons/__init__.py +++ b/langfuse/api/resources/commons/__init__.py @@ -26,6 +26,10 @@ Observation, ObservationLevel, ObservationsView, + PricingTier, + PricingTierCondition, + PricingTierInput, + PricingTierOperator, Score, ScoreConfig, ScoreDataType, @@ -82,6 +86,10 @@ "Observation", "ObservationLevel", "ObservationsView", + "PricingTier", + "PricingTierCondition", + "PricingTierInput", + "PricingTierOperator", "Score", "ScoreConfig", "ScoreDataType", diff --git a/langfuse/api/resources/commons/types/__init__.py b/langfuse/api/resources/commons/types/__init__.py index 1c0d06a8d..ee7f5714b 100644 --- a/langfuse/api/resources/commons/types/__init__.py +++ b/langfuse/api/resources/commons/types/__init__.py @@ -25,6 +25,10 @@ from .observation import Observation from .observation_level import ObservationLevel from .observations_view import ObservationsView +from .pricing_tier import PricingTier +from .pricing_tier_condition import PricingTierCondition +from .pricing_tier_input import PricingTierInput +from .pricing_tier_operator import PricingTierOperator from .score import Score, Score_Boolean, Score_Categorical, Score_Numeric from .score_config import ScoreConfig from .score_data_type import ScoreDataType @@ -63,6 +67,10 @@ "Observation", "ObservationLevel", "ObservationsView", + "PricingTier", + "PricingTierCondition", + "PricingTierInput", + "PricingTierOperator", "Score", "ScoreConfig", "ScoreDataType", diff --git a/langfuse/api/resources/commons/types/model.py b/langfuse/api/resources/commons/types/model.py index ea3922ee9..1b83c2696 100644 --- a/langfuse/api/resources/commons/types/model.py +++ b/langfuse/api/resources/commons/types/model.py @@ -7,11 +7,20 @@ from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 from .model_price import ModelPrice from .model_usage_unit import ModelUsageUnit +from .pricing_tier import PricingTier class Model(pydantic_v1.BaseModel): """ Model definition used for transforming usage into USD cost and/or tokenization. + + Models can have either simple flat pricing or tiered pricing: + - Flat pricing: Single price per usage type (legacy, but still supported) + - Tiered pricing: Multiple pricing tiers with conditional matching based on usage patterns + + The pricing tiers approach is recommended for models with usage-based pricing variations. + When using tiered pricing, the flat price fields (inputPrice, outputPrice, prices) are populated + from the default tier for backward compatibility. """ id: str @@ -73,9 +82,30 @@ class Model(pydantic_v1.BaseModel): """ is_langfuse_managed: bool = pydantic_v1.Field(alias="isLangfuseManaged") + created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") + """ + Timestamp when the model was created + """ + prices: typing.Dict[str, ModelPrice] = pydantic_v1.Field() """ - Price (USD) by usage type + Deprecated. Use 'pricingTiers' instead for models with usage-based pricing variations. + + This field shows prices by usage type from the default pricing tier. Maintained for backward compatibility. + If the model uses tiered pricing, this field will be populated from the default tier's prices. + """ + + pricing_tiers: typing.List[PricingTier] = pydantic_v1.Field(alias="pricingTiers") + """ + Array of pricing tiers with conditional pricing based on usage thresholds. + + Pricing tiers enable accurate cost tracking for models that charge different rates based on usage patterns + (e.g., different rates for high-volume usage, large context windows, or cached tokens). + + Each model must have exactly one default tier (isDefault=true, priority=0) that serves as a fallback. + Additional conditional tiers can be defined with specific matching criteria. + + If this array is empty, the model uses legacy flat pricing from the inputPrice/outputPrice/totalPrice fields. """ def json(self, **kwargs: typing.Any) -> str: diff --git a/langfuse/api/resources/commons/types/pricing_tier.py b/langfuse/api/resources/commons/types/pricing_tier.py new file mode 100644 index 000000000..031d142c0 --- /dev/null +++ b/langfuse/api/resources/commons/types/pricing_tier.py @@ -0,0 +1,117 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ....core.datetime_utils import serialize_datetime +from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 +from .pricing_tier_condition import PricingTierCondition + + +class PricingTier(pydantic_v1.BaseModel): + """ + Pricing tier definition with conditional pricing based on usage thresholds. + + Pricing tiers enable accurate cost tracking for LLM providers that charge different rates based on usage patterns. + For example, some providers charge higher rates when context size exceeds certain thresholds. + + How tier matching works: + 1. Tiers are evaluated in ascending priority order (priority 1 before priority 2, etc.) + 2. The first tier where ALL conditions match is selected + 3. If no conditional tiers match, the default tier is used as a fallback + 4. The default tier has priority 0 and no conditions + + Why priorities matter: + - Lower priority numbers are evaluated first, allowing you to define specific cases before general ones + - Example: Priority 1 for "high usage" (>200K tokens), Priority 2 for "medium usage" (>100K tokens), Priority 0 for default + - Without proper ordering, a less specific condition might match before a more specific one + + Every model must have exactly one default tier to ensure cost calculation always succeeds. + """ + + id: str = pydantic_v1.Field() + """ + Unique identifier for the pricing tier + """ + + name: str = pydantic_v1.Field() + """ + Name of the pricing tier for display and identification purposes. + + Examples: "Standard", "High Volume Tier", "Large Context", "Extended Context Tier" + """ + + is_default: bool = pydantic_v1.Field(alias="isDefault") + """ + Whether this is the default tier. Every model must have exactly one default tier with priority 0 and no conditions. + + The default tier serves as a fallback when no conditional tiers match, ensuring cost calculation always succeeds. + It typically represents the base pricing for standard usage patterns. + """ + + priority: int = pydantic_v1.Field() + """ + Priority for tier matching evaluation. Lower numbers = higher priority (evaluated first). + + The default tier must always have priority 0. Conditional tiers should have priority 1, 2, 3, etc. + + Example ordering: + - Priority 0: Default tier (no conditions, always matches as fallback) + - Priority 1: High usage tier (e.g., >200K tokens) + - Priority 2: Medium usage tier (e.g., >100K tokens) + + This ensures more specific conditions are checked before general ones. + """ + + conditions: typing.List[PricingTierCondition] = pydantic_v1.Field() + """ + Array of conditions that must ALL be met for this tier to match (AND logic). + + The default tier must have an empty conditions array. Conditional tiers should have one or more conditions + that define when this tier's pricing applies. + + Multiple conditions enable complex matching scenarios (e.g., "high input tokens AND low output tokens"). + """ + + prices: typing.Dict[str, float] = pydantic_v1.Field() + """ + Prices (USD) by usage type for this tier. + + Common usage types: "input", "output", "total", "request", "image" + Prices are specified in USD per unit (e.g., per token, per request, per second). + + Example: {"input": 0.000003, "output": 0.000015} means $3 per million input tokens and $15 per million output tokens. + """ + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + allow_population_by_field_name = True + populate_by_name = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/pricing_tier_condition.py b/langfuse/api/resources/commons/types/pricing_tier_condition.py new file mode 100644 index 000000000..8b89fe116 --- /dev/null +++ b/langfuse/api/resources/commons/types/pricing_tier_condition.py @@ -0,0 +1,92 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ....core.datetime_utils import serialize_datetime +from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 +from .pricing_tier_operator import PricingTierOperator + + +class PricingTierCondition(pydantic_v1.BaseModel): + """ + Condition for matching a pricing tier based on usage details. Used to implement tiered pricing models where costs vary based on usage thresholds. + + How it works: + 1. The regex pattern matches against usage detail keys (e.g., "input_tokens", "input_cached") + 2. Values of all matching keys are summed together + 3. The sum is compared against the threshold value using the specified operator + 4. All conditions in a tier must be met (AND logic) for the tier to match + + Common use cases: + - Threshold-based pricing: Match when accumulated usage exceeds a certain amount + - Usage-type-specific pricing: Different rates for cached vs non-cached tokens, or input vs output + - Volume-based pricing: Different rates based on total request or token count + """ + + usage_detail_pattern: str = pydantic_v1.Field(alias="usageDetailPattern") + """ + Regex pattern to match against usage detail keys. All matching keys' values are summed for threshold comparison. + + Examples: + - "^input" matches "input", "input_tokens", "input_cached", etc. + - "^(input|prompt)" matches both "input_tokens" and "prompt_tokens" + - "_cache$" matches "input_cache", "output_cache", etc. + + The pattern is case-insensitive by default. If no keys match, the sum is treated as zero. + """ + + operator: PricingTierOperator = pydantic_v1.Field() + """ + Comparison operator to apply between the summed value and the threshold. + + - gt: greater than (sum > threshold) + - gte: greater than or equal (sum >= threshold) + - lt: less than (sum < threshold) + - lte: less than or equal (sum <= threshold) + - eq: equal (sum == threshold) + - neq: not equal (sum != threshold) + """ + + value: float = pydantic_v1.Field() + """ + Threshold value for comparison. For token-based pricing, this is typically the token count threshold (e.g., 200000 for a 200K token threshold). + """ + + case_sensitive: bool = pydantic_v1.Field(alias="caseSensitive") + """ + Whether the regex pattern matching is case-sensitive. Default is false (case-insensitive matching). + """ + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + allow_population_by_field_name = True + populate_by_name = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/pricing_tier_input.py b/langfuse/api/resources/commons/types/pricing_tier_input.py new file mode 100644 index 000000000..00dfdb37b --- /dev/null +++ b/langfuse/api/resources/commons/types/pricing_tier_input.py @@ -0,0 +1,102 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ....core.datetime_utils import serialize_datetime +from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 +from .pricing_tier_condition import PricingTierCondition + + +class PricingTierInput(pydantic_v1.BaseModel): + """ + Input schema for creating a pricing tier. The tier ID will be automatically generated server-side. + + When creating a model with pricing tiers: + - Exactly one tier must have isDefault=true (the fallback tier) + - The default tier must have priority=0 and conditions=[] + - All tier names and priorities must be unique within the model + - Each tier must define at least one price + + See PricingTier for detailed information about how tiers work and why they're useful. + """ + + name: str = pydantic_v1.Field() + """ + Name of the pricing tier for display and identification purposes. + + Must be unique within the model. Common patterns: "Standard", "High Volume Tier", "Extended Context" + """ + + is_default: bool = pydantic_v1.Field(alias="isDefault") + """ + Whether this is the default tier. Exactly one tier per model must be marked as default. + + Requirements for default tier: + - Must have isDefault=true + - Must have priority=0 + - Must have empty conditions array (conditions=[]) + + The default tier acts as a fallback when no conditional tiers match. + """ + + priority: int = pydantic_v1.Field() + """ + Priority for tier matching evaluation. Lower numbers = higher priority (evaluated first). + + Must be unique within the model. The default tier must have priority=0. + Conditional tiers should use priority 1, 2, 3, etc. based on their specificity. + """ + + conditions: typing.List[PricingTierCondition] = pydantic_v1.Field() + """ + Array of conditions that must ALL be met for this tier to match (AND logic). + + The default tier must have an empty array (conditions=[]). + Conditional tiers should define one or more conditions that specify when this tier's pricing applies. + + Each condition specifies a regex pattern, operator, and threshold value for matching against usage details. + """ + + prices: typing.Dict[str, float] = pydantic_v1.Field() + """ + Prices (USD) by usage type for this tier. At least one price must be defined. + + Common usage types: "input", "output", "total", "request", "image" + Prices are in USD per unit (e.g., per token). + + Example: {"input": 0.000003, "output": 0.000015} represents $3 per million input tokens and $15 per million output tokens. + """ + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + allow_population_by_field_name = True + populate_by_name = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/pricing_tier_operator.py b/langfuse/api/resources/commons/types/pricing_tier_operator.py new file mode 100644 index 000000000..c5af10199 --- /dev/null +++ b/langfuse/api/resources/commons/types/pricing_tier_operator.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import enum +import typing + +T_Result = typing.TypeVar("T_Result") + + +class PricingTierOperator(str, enum.Enum): + """ + Comparison operators for pricing tier conditions + """ + + GT = "gt" + GTE = "gte" + LT = "lt" + LTE = "lte" + EQ = "eq" + NEQ = "neq" + + def visit( + self, + gt: typing.Callable[[], T_Result], + gte: typing.Callable[[], T_Result], + lt: typing.Callable[[], T_Result], + lte: typing.Callable[[], T_Result], + eq: typing.Callable[[], T_Result], + neq: typing.Callable[[], T_Result], + ) -> T_Result: + if self is PricingTierOperator.GT: + return gt() + if self is PricingTierOperator.GTE: + return gte() + if self is PricingTierOperator.LT: + return lt() + if self is PricingTierOperator.LTE: + return lte() + if self is PricingTierOperator.EQ: + return eq() + if self is PricingTierOperator.NEQ: + return neq() diff --git a/langfuse/api/resources/models/types/create_model_request.py b/langfuse/api/resources/models/types/create_model_request.py index b3d8a6462..3f9f80119 100644 --- a/langfuse/api/resources/models/types/create_model_request.py +++ b/langfuse/api/resources/models/types/create_model_request.py @@ -6,6 +6,7 @@ from ....core.datetime_utils import serialize_datetime from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 from ...commons.types.model_usage_unit import ModelUsageUnit +from ...commons.types.pricing_tier_input import PricingTierInput class CreateModelRequest(pydantic_v1.BaseModel): @@ -35,21 +36,45 @@ class CreateModelRequest(pydantic_v1.BaseModel): alias="inputPrice", default=None ) """ - Price (USD) per input unit + Deprecated. Use 'pricingTiers' instead. Price (USD) per input unit. Creates a default tier if pricingTiers not provided. """ output_price: typing.Optional[float] = pydantic_v1.Field( alias="outputPrice", default=None ) """ - Price (USD) per output unit + Deprecated. Use 'pricingTiers' instead. Price (USD) per output unit. Creates a default tier if pricingTiers not provided. """ total_price: typing.Optional[float] = pydantic_v1.Field( alias="totalPrice", default=None ) """ - Price (USD) per total units. Cannot be set if input or output price is set. + Deprecated. Use 'pricingTiers' instead. Price (USD) per total units. Cannot be set if input or output price is set. Creates a default tier if pricingTiers not provided. + """ + + pricing_tiers: typing.Optional[typing.List[PricingTierInput]] = pydantic_v1.Field( + alias="pricingTiers", default=None + ) + """ + Optional. Array of pricing tiers for this model. + + Use pricing tiers for all models - both those with threshold-based pricing variations and those with simple flat pricing: + + - For models with standard flat pricing: Create a single default tier with your prices + (e.g., one tier with isDefault=true, priority=0, conditions=[], and your standard prices) + + - For models with threshold-based pricing: Create a default tier plus additional conditional tiers + (e.g., default tier for standard usage + high-volume tier for usage above certain thresholds) + + Requirements: + - Cannot be provided with flat prices (inputPrice/outputPrice/totalPrice) - use one approach or the other + - Must include exactly one default tier with isDefault=true, priority=0, and conditions=[] + - All tier names and priorities must be unique within the model + - Each tier must define at least one price + + If omitted, you must provide flat prices instead (inputPrice/outputPrice/totalPrice), + which will automatically create a single default tier named "Standard". """ tokenizer_id: typing.Optional[str] = pydantic_v1.Field(