arize-phoenix 10.14.0__py3-none-any.whl → 11.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (84) hide show
  1. {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/METADATA +3 -2
  2. {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/RECORD +82 -50
  3. phoenix/config.py +5 -2
  4. phoenix/datetime_utils.py +8 -1
  5. phoenix/db/bulk_inserter.py +40 -1
  6. phoenix/db/facilitator.py +263 -4
  7. phoenix/db/insertion/helpers.py +15 -0
  8. phoenix/db/insertion/span.py +3 -1
  9. phoenix/db/migrations/versions/a20694b15f82_cost.py +196 -0
  10. phoenix/db/models.py +267 -9
  11. phoenix/db/types/model_provider.py +1 -0
  12. phoenix/db/types/token_price_customization.py +29 -0
  13. phoenix/server/api/context.py +38 -4
  14. phoenix/server/api/dataloaders/__init__.py +41 -5
  15. phoenix/server/api/dataloaders/last_used_times_by_generative_model_id.py +35 -0
  16. phoenix/server/api/dataloaders/span_cost_by_span.py +24 -0
  17. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_generative_model.py +56 -0
  18. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_project_session.py +57 -0
  19. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_span.py +43 -0
  20. phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_trace.py +56 -0
  21. phoenix/server/api/dataloaders/span_cost_details_by_span_cost.py +27 -0
  22. phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py +58 -0
  23. phoenix/server/api/dataloaders/span_cost_summary_by_experiment_run.py +58 -0
  24. phoenix/server/api/dataloaders/span_cost_summary_by_generative_model.py +55 -0
  25. phoenix/server/api/dataloaders/span_cost_summary_by_project.py +140 -0
  26. phoenix/server/api/dataloaders/span_cost_summary_by_project_session.py +56 -0
  27. phoenix/server/api/dataloaders/span_cost_summary_by_trace.py +55 -0
  28. phoenix/server/api/dataloaders/span_costs.py +35 -0
  29. phoenix/server/api/dataloaders/types.py +29 -0
  30. phoenix/server/api/helpers/playground_clients.py +562 -12
  31. phoenix/server/api/helpers/prompts/conversions/aws.py +83 -0
  32. phoenix/server/api/helpers/prompts/models.py +67 -0
  33. phoenix/server/api/input_types/GenerativeModelInput.py +2 -0
  34. phoenix/server/api/input_types/ProjectSessionSort.py +3 -0
  35. phoenix/server/api/input_types/SpanSort.py +17 -0
  36. phoenix/server/api/mutations/__init__.py +2 -0
  37. phoenix/server/api/mutations/chat_mutations.py +17 -0
  38. phoenix/server/api/mutations/model_mutations.py +208 -0
  39. phoenix/server/api/queries.py +82 -41
  40. phoenix/server/api/routers/v1/traces.py +11 -4
  41. phoenix/server/api/subscriptions.py +36 -2
  42. phoenix/server/api/types/CostBreakdown.py +15 -0
  43. phoenix/server/api/types/Experiment.py +59 -1
  44. phoenix/server/api/types/ExperimentRun.py +58 -4
  45. phoenix/server/api/types/GenerativeModel.py +143 -2
  46. phoenix/server/api/types/GenerativeProvider.py +33 -20
  47. phoenix/server/api/types/{Model.py → InferenceModel.py} +1 -1
  48. phoenix/server/api/types/ModelInterface.py +11 -0
  49. phoenix/server/api/types/PlaygroundModel.py +10 -0
  50. phoenix/server/api/types/Project.py +42 -0
  51. phoenix/server/api/types/ProjectSession.py +44 -0
  52. phoenix/server/api/types/Span.py +137 -0
  53. phoenix/server/api/types/SpanCostDetailSummaryEntry.py +10 -0
  54. phoenix/server/api/types/SpanCostSummary.py +10 -0
  55. phoenix/server/api/types/TokenPrice.py +16 -0
  56. phoenix/server/api/types/TokenUsage.py +3 -3
  57. phoenix/server/api/types/Trace.py +41 -0
  58. phoenix/server/app.py +59 -0
  59. phoenix/server/cost_tracking/cost_details_calculator.py +190 -0
  60. phoenix/server/cost_tracking/cost_model_lookup.py +151 -0
  61. phoenix/server/cost_tracking/helpers.py +68 -0
  62. phoenix/server/cost_tracking/model_cost_manifest.json +59 -329
  63. phoenix/server/cost_tracking/regex_specificity.py +397 -0
  64. phoenix/server/cost_tracking/token_cost_calculator.py +57 -0
  65. phoenix/server/daemons/__init__.py +0 -0
  66. phoenix/server/daemons/generative_model_store.py +51 -0
  67. phoenix/server/daemons/span_cost_calculator.py +103 -0
  68. phoenix/server/dml_event_handler.py +1 -0
  69. phoenix/server/static/.vite/manifest.json +36 -36
  70. phoenix/server/static/assets/components-BnK9kodr.js +5055 -0
  71. phoenix/server/static/assets/{index-qiubV_74.js → index-S3YKLmbo.js} +13 -13
  72. phoenix/server/static/assets/{pages-C4V07ozl.js → pages-BW6PBHZb.js} +809 -417
  73. phoenix/server/static/assets/{vendor-Bfsiga8H.js → vendor-DqQvHbPa.js} +147 -147
  74. phoenix/server/static/assets/{vendor-arizeai-CQOWsrzm.js → vendor-arizeai-CLX44PFA.js} +1 -1
  75. phoenix/server/static/assets/{vendor-codemirror-CrcGVhB2.js → vendor-codemirror-Du3XyJnB.js} +1 -1
  76. phoenix/server/static/assets/{vendor-recharts-Yyg3G-Rq.js → vendor-recharts-B2PJDrnX.js} +25 -25
  77. phoenix/server/static/assets/{vendor-shiki-OPjag7Hm.js → vendor-shiki-CNbrFjf9.js} +1 -1
  78. phoenix/version.py +1 -1
  79. phoenix/server/cost_tracking/cost_lookup.py +0 -255
  80. phoenix/server/static/assets/components-CUUWyAMo.js +0 -4509
  81. {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/WHEEL +0 -0
  82. {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/entry_points.txt +0 -0
  83. {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/licenses/IP_NOTICE +0 -0
  84. {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/licenses/LICENSE +0 -0
phoenix/db/models.py CHANGED
@@ -1,12 +1,15 @@
1
+ import re
1
2
  from datetime import datetime, timezone
2
3
  from typing import Any, Iterable, Literal, Optional, Sequence, TypedDict, cast
3
4
 
5
+ import sqlalchemy as sa
4
6
  import sqlalchemy.sql as sql
5
7
  from openinference.semconv.trace import RerankerAttributes, SpanAttributes
6
8
  from sqlalchemy import (
7
9
  JSON,
8
10
  NUMERIC,
9
11
  TIMESTAMP,
12
+ Boolean,
10
13
  CheckConstraint,
11
14
  ColumnElement,
12
15
  Dialect,
@@ -52,6 +55,10 @@ from phoenix.db.types.annotation_configs import (
52
55
  )
53
56
  from phoenix.db.types.identifier import Identifier
54
57
  from phoenix.db.types.model_provider import ModelProvider
58
+ from phoenix.db.types.token_price_customization import (
59
+ TokenPriceCustomization,
60
+ TokenPriceCustomizationParser,
61
+ )
55
62
  from phoenix.db.types.trace_retention import TraceRetentionCronExpression, TraceRetentionRule
56
63
  from phoenix.server.api.helpers.prompts.models import (
57
64
  PromptInvocationParameters,
@@ -391,12 +398,49 @@ class _AnnotationConfig(TypeDecorator[AnnotationConfigType]):
391
398
  return AnnotationConfigModel.model_validate(value).root if value is not None else None
392
399
 
393
400
 
401
+ class _TokenCustomization(TypeDecorator[TokenPriceCustomization]):
402
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
403
+ cache_ok = True
404
+ impl = JSON
405
+
406
+ def process_bind_param(
407
+ self, value: Optional[TokenPriceCustomization], _: Dialect
408
+ ) -> Optional[dict[str, Any]]:
409
+ return value.model_dump() if value is not None else None
410
+
411
+ def process_result_value(
412
+ self, value: Optional[dict[str, Any]], _: Dialect
413
+ ) -> Optional[TokenPriceCustomization]:
414
+ return TokenPriceCustomizationParser.parse(value)
415
+
416
+
417
+ class _RegexStr(TypeDecorator[re.Pattern[str]]):
418
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
419
+ cache_ok = True
420
+ impl = String
421
+
422
+ def process_bind_param(self, value: Optional[re.Pattern[str]], _: Dialect) -> Optional[str]:
423
+ if value is None:
424
+ return None
425
+ if not isinstance(value, re.Pattern):
426
+ raise TypeError(f"Expected a regex pattern, got {type(value)}")
427
+ pattern = value.pattern
428
+ if not isinstance(pattern, str):
429
+ raise ValueError(f"Expected a string, got {type(pattern)}")
430
+ return pattern
431
+
432
+ def process_result_value(self, value: Optional[str], _: Dialect) -> Optional[re.Pattern[str]]:
433
+ if value is None:
434
+ return None
435
+ return re.compile(value)
436
+
437
+
394
438
  class ExperimentRunOutput(TypedDict, total=False):
395
439
  task_output: Any
396
440
 
397
441
 
398
442
  class Base(DeclarativeBase):
399
- id: Mapped[int] = mapped_column(Integer, primary_key=True)
443
+ id: Mapped[int] = mapped_column(primary_key=True)
400
444
  # Enforce best practices for naming constraints
401
445
  # https://alembic.sqlalchemy.org/en/latest/naming.html#integration-of-naming-conventions-into-operations-autogenerate
402
446
  metadata = MetaData(
@@ -418,7 +462,6 @@ class Base(DeclarativeBase):
418
462
 
419
463
  class ProjectTraceRetentionPolicy(Base):
420
464
  __tablename__ = "project_trace_retention_policies"
421
- id: Mapped[int] = mapped_column(Integer, primary_key=True)
422
465
  name: Mapped[str] = mapped_column(String, nullable=False)
423
466
  cron_expression: Mapped[TraceRetentionCronExpression] = mapped_column(
424
467
  _TraceRetentionCronExpression, nullable=False
@@ -528,6 +571,12 @@ class Trace(Base):
528
571
  primaryjoin="foreign(ExperimentRun.trace_id) == Trace.trace_id",
529
572
  back_populates="trace",
530
573
  )
574
+ span_costs: Mapped[list["SpanCost"]] = relationship(
575
+ "SpanCost",
576
+ back_populates="trace",
577
+ cascade="all, delete-orphan",
578
+ uselist=True,
579
+ )
531
580
  __table_args__ = (
532
581
  UniqueConstraint(
533
582
  "trace_id",
@@ -685,6 +734,7 @@ class Span(Base):
685
734
  span_annotations: Mapped[list["SpanAnnotation"]] = relationship(back_populates="span")
686
735
  document_annotations: Mapped[list["DocumentAnnotation"]] = relationship(back_populates="span")
687
736
  dataset_examples: Mapped[list["DatasetExample"]] = relationship(back_populates="span")
737
+ span_cost: Mapped[Optional["SpanCost"]] = relationship(back_populates="span")
688
738
 
689
739
  __table_args__ = (
690
740
  UniqueConstraint(
@@ -1303,9 +1353,86 @@ class ApiKey(Base):
1303
1353
  __table_args__ = (dict(sqlite_autoincrement=True),)
1304
1354
 
1305
1355
 
1356
+ CostType: TypeAlias = Literal["DEFAULT", "OVERRIDE"]
1357
+
1358
+
1359
+ class GenerativeModel(Base):
1360
+ __tablename__ = "generative_models"
1361
+ name: Mapped[str] = mapped_column(String, nullable=False)
1362
+ provider: Mapped[str]
1363
+ start_time: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
1364
+ name_pattern: Mapped[re.Pattern[str]] = mapped_column(_RegexStr, nullable=False)
1365
+ is_built_in: Mapped[bool] = mapped_column(
1366
+ Boolean,
1367
+ nullable=False,
1368
+ )
1369
+ created_at: Mapped[datetime] = mapped_column(
1370
+ UtcTimeStamp,
1371
+ server_default=func.now(),
1372
+ )
1373
+ updated_at: Mapped[datetime] = mapped_column(
1374
+ UtcTimeStamp,
1375
+ server_default=func.now(),
1376
+ onupdate=func.now(),
1377
+ )
1378
+ deleted_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
1379
+
1380
+ token_prices: Mapped[list["TokenPrice"]] = relationship(
1381
+ "TokenPrice",
1382
+ back_populates="model",
1383
+ cascade="all, delete-orphan",
1384
+ uselist=True,
1385
+ )
1386
+
1387
+ __table_args__ = (
1388
+ Index(
1389
+ "ix_generative_models_match_criteria",
1390
+ "name_pattern",
1391
+ "provider",
1392
+ "is_built_in",
1393
+ postgresql_where=sa.text("deleted_at IS NULL"),
1394
+ sqlite_where=sa.text("deleted_at IS NULL"),
1395
+ unique=True,
1396
+ ),
1397
+ Index(
1398
+ "ix_generative_models_name_is_built_in",
1399
+ "name",
1400
+ "is_built_in",
1401
+ postgresql_where=sa.text("deleted_at IS NULL"),
1402
+ sqlite_where=sa.text("deleted_at IS NULL"),
1403
+ unique=True,
1404
+ ),
1405
+ )
1406
+
1407
+
1408
+ class TokenPrice(Base):
1409
+ __tablename__ = "token_prices"
1410
+ model_id: Mapped[int] = mapped_column(
1411
+ ForeignKey("generative_models.id", ondelete="CASCADE"),
1412
+ nullable=False,
1413
+ index=True,
1414
+ )
1415
+ token_type: Mapped[str]
1416
+ is_prompt: Mapped[bool]
1417
+ base_rate: Mapped[float]
1418
+ customization: Mapped[TokenPriceCustomization] = mapped_column(_TokenCustomization)
1419
+
1420
+ model: Mapped["GenerativeModel"] = relationship(
1421
+ "GenerativeModel",
1422
+ back_populates="token_prices",
1423
+ )
1424
+
1425
+ __table_args__ = (
1426
+ UniqueConstraint(
1427
+ "model_id",
1428
+ "token_type",
1429
+ "is_prompt",
1430
+ ),
1431
+ )
1432
+
1433
+
1306
1434
  class PromptLabel(Base):
1307
1435
  __tablename__ = "prompt_labels"
1308
-
1309
1436
  name: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
1310
1437
  description: Mapped[Optional[str]]
1311
1438
  color: Mapped[str] = mapped_column(String, nullable=True)
@@ -1320,7 +1447,6 @@ class PromptLabel(Base):
1320
1447
 
1321
1448
  class Prompt(Base):
1322
1449
  __tablename__ = "prompts"
1323
-
1324
1450
  source_prompt_id: Mapped[Optional[int]] = mapped_column(
1325
1451
  ForeignKey("prompts.id", ondelete="SET NULL"),
1326
1452
  index=True,
@@ -1358,7 +1484,6 @@ class Prompt(Base):
1358
1484
 
1359
1485
  class PromptPromptLabel(Base):
1360
1486
  __tablename__ = "prompts_prompt_labels"
1361
-
1362
1487
  prompt_label_id: Mapped[int] = mapped_column(
1363
1488
  ForeignKey("prompt_labels.id", ondelete="CASCADE"),
1364
1489
  index=True,
@@ -1458,16 +1583,12 @@ class PromptVersionTag(Base):
1458
1583
 
1459
1584
  class AnnotationConfig(Base):
1460
1585
  __tablename__ = "annotation_configs"
1461
-
1462
- id: Mapped[int] = mapped_column(primary_key=True)
1463
1586
  name: Mapped[str] = mapped_column(String, nullable=False, unique=True)
1464
1587
  config: Mapped[AnnotationConfigType] = mapped_column(_AnnotationConfig, nullable=False)
1465
1588
 
1466
1589
 
1467
1590
  class ProjectAnnotationConfig(Base):
1468
1591
  __tablename__ = "project_annotation_configs"
1469
-
1470
- id: Mapped[int] = mapped_column(primary_key=True)
1471
1592
  project_id: Mapped[int] = mapped_column(
1472
1593
  ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
1473
1594
  )
@@ -1476,3 +1597,140 @@ class ProjectAnnotationConfig(Base):
1476
1597
  )
1477
1598
 
1478
1599
  __table_args__ = (UniqueConstraint("project_id", "annotation_config_id"),)
1600
+
1601
+
1602
+ class SpanCost(Base):
1603
+ __tablename__ = "span_costs"
1604
+
1605
+ span_rowid: Mapped[int] = mapped_column(
1606
+ ForeignKey("spans.id", ondelete="CASCADE"),
1607
+ nullable=False,
1608
+ )
1609
+ trace_rowid: Mapped[int] = mapped_column(
1610
+ ForeignKey("traces.id", ondelete="CASCADE"),
1611
+ nullable=False,
1612
+ )
1613
+ span_start_time: Mapped[datetime] = mapped_column(
1614
+ UtcTimeStamp,
1615
+ nullable=False,
1616
+ index=True,
1617
+ )
1618
+ model_id: Mapped[Optional[int]] = mapped_column(
1619
+ sa.Integer,
1620
+ ForeignKey(
1621
+ "generative_models.id",
1622
+ ondelete="RESTRICT",
1623
+ ),
1624
+ nullable=True,
1625
+ )
1626
+ total_cost: Mapped[Optional[float]]
1627
+ total_tokens: Mapped[Optional[float]]
1628
+
1629
+ @hybrid_property
1630
+ def total_cost_per_token(self) -> Optional[float]:
1631
+ return ((self.total_cost or 0) / self.total_tokens) if self.total_tokens else None
1632
+
1633
+ @total_cost_per_token.inplace.expression
1634
+ @classmethod
1635
+ def _total_cost_per_token_expression(cls) -> ColumnElement[Optional[float]]:
1636
+ return sql.case(
1637
+ (
1638
+ sa.and_(cls.total_tokens.isnot(None), cls.total_tokens != 0),
1639
+ cls.total_cost / cls.total_tokens,
1640
+ )
1641
+ )
1642
+
1643
+ prompt_cost: Mapped[Optional[float]]
1644
+ prompt_tokens: Mapped[Optional[float]]
1645
+
1646
+ @hybrid_property
1647
+ def prompt_cost_per_token(self) -> Optional[float]:
1648
+ return ((self.prompt_cost or 0) / self.prompt_tokens) if self.prompt_tokens else None
1649
+
1650
+ @prompt_cost_per_token.inplace.expression
1651
+ @classmethod
1652
+ def _prompt_cost_per_token_expression(cls) -> ColumnElement[Optional[float]]:
1653
+ return sql.case(
1654
+ (
1655
+ sa.and_(cls.prompt_tokens.isnot(None), cls.prompt_tokens != 0),
1656
+ cls.prompt_cost / cls.prompt_tokens,
1657
+ )
1658
+ )
1659
+
1660
+ completion_cost: Mapped[Optional[float]]
1661
+ completion_tokens: Mapped[Optional[float]]
1662
+
1663
+ @hybrid_property
1664
+ def completion_cost_per_token(self) -> Optional[float]:
1665
+ return (
1666
+ ((self.completion_cost or 0) / self.completion_tokens)
1667
+ if self.completion_tokens
1668
+ else None
1669
+ )
1670
+
1671
+ @completion_cost_per_token.inplace.expression
1672
+ @classmethod
1673
+ def _completion_cost_per_token_expression(cls) -> ColumnElement[Optional[float]]:
1674
+ return sql.case(
1675
+ (
1676
+ sa.and_(cls.completion_tokens.isnot(None), cls.completion_tokens != 0),
1677
+ cls.completion_cost / cls.completion_tokens,
1678
+ )
1679
+ )
1680
+
1681
+ span: Mapped["Span"] = relationship("Span", back_populates="span_cost")
1682
+ trace: Mapped["Trace"] = relationship("Trace", back_populates="span_costs")
1683
+ span_cost_details: Mapped[list["SpanCostDetail"]] = relationship(
1684
+ "SpanCostDetail",
1685
+ back_populates="span_cost",
1686
+ cascade="all, delete-orphan",
1687
+ uselist=True,
1688
+ )
1689
+
1690
+ __table_args__ = (
1691
+ Index(
1692
+ "ix_span_costs_model_id_span_start_time",
1693
+ "model_id",
1694
+ "span_start_time",
1695
+ ),
1696
+ )
1697
+
1698
+ def append_detail(self, detail: "SpanCostDetail") -> None:
1699
+ self.span_cost_details.append(detail)
1700
+ if cost := detail.cost:
1701
+ if detail.is_prompt:
1702
+ self.prompt_cost = (self.prompt_cost or 0) + cost
1703
+ else:
1704
+ self.completion_cost = (self.completion_cost or 0) + cost
1705
+ self.total_cost = (self.total_cost or 0) + cost
1706
+ if tokens := detail.tokens:
1707
+ if detail.is_prompt:
1708
+ self.prompt_tokens = (self.prompt_tokens or 0) + tokens
1709
+ else:
1710
+ self.completion_tokens = (self.completion_tokens or 0) + tokens
1711
+ self.total_tokens = (self.total_tokens or 0) + tokens
1712
+
1713
+
1714
+ class SpanCostDetail(Base):
1715
+ __tablename__ = "span_cost_details"
1716
+ span_cost_id: Mapped[int] = mapped_column(
1717
+ ForeignKey("span_costs.id", ondelete="CASCADE"),
1718
+ nullable=False,
1719
+ index=True,
1720
+ )
1721
+ token_type: Mapped[str]
1722
+ is_prompt: Mapped[bool]
1723
+
1724
+ cost: Mapped[Optional[float]]
1725
+ tokens: Mapped[Optional[float]]
1726
+ cost_per_token: Mapped[Optional[float]]
1727
+
1728
+ span_cost: Mapped["SpanCost"] = relationship("SpanCost", back_populates="span_cost_details")
1729
+
1730
+ __table_args__ = (
1731
+ UniqueConstraint(
1732
+ "span_cost_id",
1733
+ "token_type",
1734
+ "is_prompt",
1735
+ ),
1736
+ )
@@ -9,3 +9,4 @@ class ModelProvider(Enum):
9
9
  DEEPSEEK = "DEEPSEEK"
10
10
  XAI = "XAI"
11
11
  OLLAMA = "OLLAMA"
12
+ AWS = "AWS"
@@ -0,0 +1,29 @@
1
+ from abc import ABC
2
+ from typing import Any, Literal, Optional
3
+
4
+ from pydantic import BaseModel, ValidationError
5
+
6
+
7
+ class TokenPriceCustomization(BaseModel, ABC):
8
+ model_config = {"extra": "allow"}
9
+
10
+
11
+ class ThresholdBasedTokenPriceCustomization(TokenPriceCustomization):
12
+ type: Literal["threshold_based"] = "threshold_based"
13
+ key: str
14
+ threshold: float
15
+ new_rate: float
16
+
17
+
18
+ class TokenPriceCustomizationParser:
19
+ """Intended to be forward-compatible while maintaining the ability to round-trip."""
20
+
21
+ @staticmethod
22
+ def parse(data: Optional[dict[str, Any]]) -> Optional[TokenPriceCustomization]:
23
+ if not data:
24
+ return None
25
+ try:
26
+ return ThresholdBasedTokenPriceCustomization.model_validate(data)
27
+ except ValidationError:
28
+ pass
29
+ return TokenPriceCustomization.model_validate(data)
@@ -28,6 +28,7 @@ from phoenix.server.api.dataloaders import (
28
28
  ExperimentRunAnnotations,
29
29
  ExperimentRunCountsDataLoader,
30
30
  ExperimentSequenceNumberDataLoader,
31
+ LastUsedTimesByGenerativeModelIdDataLoader,
31
32
  LatencyMsQuantileDataLoader,
32
33
  MinStartOrMaxEndTimeDataLoader,
33
34
  NumChildSpansDataLoader,
@@ -43,6 +44,18 @@ from phoenix.server.api.dataloaders import (
43
44
  SessionTraceLatencyMsQuantileDataLoader,
44
45
  SpanAnnotationsDataLoader,
45
46
  SpanByIdDataLoader,
47
+ SpanCostBySpanDataLoader,
48
+ SpanCostDetailsBySpanCostDataLoader,
49
+ SpanCostDetailSummaryEntriesByGenerativeModelDataLoader,
50
+ SpanCostDetailSummaryEntriesByProjectSessionDataLoader,
51
+ SpanCostDetailSummaryEntriesBySpanDataLoader,
52
+ SpanCostDetailSummaryEntriesByTraceDataLoader,
53
+ SpanCostSummaryByExperimentDataLoader,
54
+ SpanCostSummaryByExperimentRunDataLoader,
55
+ SpanCostSummaryByGenerativeModelDataLoader,
56
+ SpanCostSummaryByProjectDataLoader,
57
+ SpanCostSummaryByProjectSessionDataLoader,
58
+ SpanCostSummaryByTraceDataLoader,
46
59
  SpanDatasetExamplesDataLoader,
47
60
  SpanDescendantsDataLoader,
48
61
  SpanProjectsDataLoader,
@@ -55,6 +68,7 @@ from phoenix.server.api.dataloaders import (
55
68
  UsersDataLoader,
56
69
  )
57
70
  from phoenix.server.bearer_auth import PhoenixUser
71
+ from phoenix.server.daemons.span_cost_calculator import SpanCostCalculator
58
72
  from phoenix.server.dml_event import DmlEvent
59
73
  from phoenix.server.email.types import EmailSender
60
74
  from phoenix.server.types import (
@@ -68,23 +82,26 @@ from phoenix.server.types import (
68
82
 
69
83
  @dataclass
70
84
  class DataLoaders:
85
+ annotation_summaries: AnnotationSummaryDataLoader
71
86
  average_experiment_run_latency: AverageExperimentRunLatencyDataLoader
72
87
  dataset_example_revisions: DatasetExampleRevisionsDataLoader
73
88
  dataset_example_spans: DatasetExampleSpansDataLoader
74
89
  document_evaluation_summaries: DocumentEvaluationSummaryDataLoader
75
90
  document_evaluations: DocumentEvaluationsDataLoader
76
91
  document_retrieval_metrics: DocumentRetrievalMetricsDataLoader
77
- annotation_summaries: AnnotationSummaryDataLoader
78
92
  experiment_annotation_summaries: ExperimentAnnotationSummaryDataLoader
79
93
  experiment_error_rates: ExperimentErrorRatesDataLoader
80
94
  experiment_run_annotations: ExperimentRunAnnotations
81
95
  experiment_run_counts: ExperimentRunCountsDataLoader
82
96
  experiment_sequence_number: ExperimentSequenceNumberDataLoader
97
+ last_used_times_by_generative_model_id: LastUsedTimesByGenerativeModelIdDataLoader
83
98
  latency_ms_quantile: LatencyMsQuantileDataLoader
84
99
  min_start_or_max_end_times: MinStartOrMaxEndTimeDataLoader
85
100
  num_child_spans: NumChildSpansDataLoader
86
101
  num_spans_per_trace: NumSpansPerTraceDataLoader
102
+ project_by_name: ProjectByNameDataLoader
87
103
  project_fields: TableFieldsDataLoader
104
+ project_trace_retention_policy_fields: TableFieldsDataLoader
88
105
  projects_by_trace_retention_policy_id: ProjectIdsByTraceRetentionPolicyIdDataLoader
89
106
  prompt_version_sequence_number: PromptVersionSequenceNumberDataLoader
90
107
  record_counts: RecordCountDataLoader
@@ -96,6 +113,24 @@ class DataLoaders:
96
113
  session_trace_latency_ms_quantile: SessionTraceLatencyMsQuantileDataLoader
97
114
  span_annotations: SpanAnnotationsDataLoader
98
115
  span_by_id: SpanByIdDataLoader
116
+ span_cost_by_span: SpanCostBySpanDataLoader
117
+ span_cost_detail_fields: TableFieldsDataLoader
118
+ span_cost_detail_summary_entries_by_generative_model: (
119
+ SpanCostDetailSummaryEntriesByGenerativeModelDataLoader
120
+ )
121
+ span_cost_detail_summary_entries_by_project_session: (
122
+ SpanCostDetailSummaryEntriesByProjectSessionDataLoader
123
+ )
124
+ span_cost_detail_summary_entries_by_span: SpanCostDetailSummaryEntriesBySpanDataLoader
125
+ span_cost_detail_summary_entries_by_trace: SpanCostDetailSummaryEntriesByTraceDataLoader
126
+ span_cost_details_by_span_cost: SpanCostDetailsBySpanCostDataLoader
127
+ span_cost_fields: TableFieldsDataLoader
128
+ span_cost_summary_by_experiment: SpanCostSummaryByExperimentDataLoader
129
+ span_cost_summary_by_experiment_run: SpanCostSummaryByExperimentRunDataLoader
130
+ span_cost_summary_by_generative_model: SpanCostSummaryByGenerativeModelDataLoader
131
+ span_cost_summary_by_project: SpanCostSummaryByProjectDataLoader
132
+ span_cost_summary_by_project_session: SpanCostSummaryByProjectSessionDataLoader
133
+ span_cost_summary_by_trace: SpanCostSummaryByTraceDataLoader
99
134
  span_dataset_examples: SpanDatasetExamplesDataLoader
100
135
  span_descendants: SpanDescendantsDataLoader
101
136
  span_fields: TableFieldsDataLoader
@@ -104,11 +139,9 @@ class DataLoaders:
104
139
  trace_by_trace_ids: TraceByTraceIdsDataLoader
105
140
  trace_fields: TableFieldsDataLoader
106
141
  trace_retention_policy_id_by_project_id: TraceRetentionPolicyIdByProjectIdDataLoader
107
- project_trace_retention_policy_fields: TableFieldsDataLoader
108
142
  trace_root_spans: TraceRootSpansDataLoader
109
- project_by_name: ProjectByNameDataLoader
110
- users: UsersDataLoader
111
143
  user_roles: UserRolesDataLoader
144
+ users: UsersDataLoader
112
145
 
113
146
 
114
147
  class _NoOp:
@@ -123,6 +156,7 @@ class Context(BaseContext):
123
156
  cache_for_dataloaders: Optional[CacheForDataLoaders]
124
157
  model: Model
125
158
  export_path: Path
159
+ span_cost_calculator: SpanCostCalculator
126
160
  last_updated_at: CanGetLastUpdatedAt = _NoOp()
127
161
  event_queue: CanPutItem[DmlEvent] = _NoOp()
128
162
  corpus: Optional[Model] = None
@@ -1,5 +1,9 @@
1
1
  from dataclasses import dataclass, field
2
2
 
3
+ from phoenix.server.api.dataloaders.span_cost_detail_summary_entries_by_project_session import (
4
+ SpanCostDetailSummaryEntriesByProjectSessionDataLoader,
5
+ )
6
+
3
7
  from .annotation_summaries import AnnotationSummaryCache, AnnotationSummaryDataLoader
4
8
  from .average_experiment_run_latency import AverageExperimentRunLatencyDataLoader
5
9
  from .dataset_example_revisions import DatasetExampleRevisionsDataLoader
@@ -15,6 +19,7 @@ from .experiment_error_rates import ExperimentErrorRatesDataLoader
15
19
  from .experiment_run_annotations import ExperimentRunAnnotations
16
20
  from .experiment_run_counts import ExperimentRunCountsDataLoader
17
21
  from .experiment_sequence_number import ExperimentSequenceNumberDataLoader
22
+ from .last_used_times_by_generative_model_id import LastUsedTimesByGenerativeModelIdDataLoader
18
23
  from .latency_ms_quantile import LatencyMsQuantileCache, LatencyMsQuantileDataLoader
19
24
  from .min_start_or_max_end_times import MinStartOrMaxEndTimeCache, MinStartOrMaxEndTimeDataLoader
20
25
  from .num_child_spans import NumChildSpansDataLoader
@@ -30,6 +35,20 @@ from .session_token_usages import SessionTokenUsagesDataLoader
30
35
  from .session_trace_latency_ms_quantile import SessionTraceLatencyMsQuantileDataLoader
31
36
  from .span_annotations import SpanAnnotationsDataLoader
32
37
  from .span_by_id import SpanByIdDataLoader
38
+ from .span_cost_by_span import SpanCostBySpanDataLoader
39
+ from .span_cost_detail_summary_entries_by_generative_model import (
40
+ SpanCostDetailSummaryEntriesByGenerativeModelDataLoader,
41
+ )
42
+ from .span_cost_detail_summary_entries_by_span import SpanCostDetailSummaryEntriesBySpanDataLoader
43
+ from .span_cost_detail_summary_entries_by_trace import SpanCostDetailSummaryEntriesByTraceDataLoader
44
+ from .span_cost_details_by_span_cost import SpanCostDetailsBySpanCostDataLoader
45
+ from .span_cost_summary_by_experiment import SpanCostSummaryByExperimentDataLoader
46
+ from .span_cost_summary_by_experiment_run import SpanCostSummaryByExperimentRunDataLoader
47
+ from .span_cost_summary_by_generative_model import SpanCostSummaryByGenerativeModelDataLoader
48
+ from .span_cost_summary_by_project import SpanCostSummaryByProjectDataLoader, SpanCostSummaryCache
49
+ from .span_cost_summary_by_project_session import SpanCostSummaryByProjectSessionDataLoader
50
+ from .span_cost_summary_by_trace import SpanCostSummaryByTraceDataLoader
51
+ from .span_costs import SpanCostsDataLoader
33
52
  from .span_dataset_examples import SpanDatasetExamplesDataLoader
34
53
  from .span_descendants import SpanDescendantsDataLoader
35
54
  from .span_projects import SpanProjectsDataLoader
@@ -42,23 +61,25 @@ from .user_roles import UserRolesDataLoader
42
61
  from .users import UsersDataLoader
43
62
 
44
63
  __all__ = [
45
- "CacheForDataLoaders",
64
+ "AnnotationSummaryDataLoader",
46
65
  "AverageExperimentRunLatencyDataLoader",
66
+ "CacheForDataLoaders",
47
67
  "DatasetExampleRevisionsDataLoader",
48
68
  "DatasetExampleSpansDataLoader",
49
69
  "DocumentEvaluationSummaryDataLoader",
50
70
  "DocumentEvaluationsDataLoader",
51
71
  "DocumentRetrievalMetricsDataLoader",
52
- "AnnotationSummaryDataLoader",
53
72
  "ExperimentAnnotationSummaryDataLoader",
54
73
  "ExperimentErrorRatesDataLoader",
55
74
  "ExperimentRunAnnotations",
56
75
  "ExperimentRunCountsDataLoader",
57
76
  "ExperimentSequenceNumberDataLoader",
77
+ "LastUsedTimesByGenerativeModelIdDataLoader",
58
78
  "LatencyMsQuantileDataLoader",
59
79
  "MinStartOrMaxEndTimeDataLoader",
60
80
  "NumChildSpansDataLoader",
61
81
  "NumSpansPerTraceDataLoader",
82
+ "ProjectByNameDataLoader",
62
83
  "ProjectIdsByTraceRetentionPolicyIdDataLoader",
63
84
  "PromptVersionSequenceNumberDataLoader",
64
85
  "RecordCountDataLoader",
@@ -67,7 +88,21 @@ __all__ = [
67
88
  "SessionNumTracesWithErrorDataLoader",
68
89
  "SessionTokenUsagesDataLoader",
69
90
  "SessionTraceLatencyMsQuantileDataLoader",
91
+ "SpanAnnotationsDataLoader",
70
92
  "SpanByIdDataLoader",
93
+ "SpanCostBySpanDataLoader",
94
+ "SpanCostDetailSummaryEntriesByGenerativeModelDataLoader",
95
+ "SpanCostDetailSummaryEntriesByProjectSessionDataLoader",
96
+ "SpanCostDetailSummaryEntriesBySpanDataLoader",
97
+ "SpanCostDetailSummaryEntriesByTraceDataLoader",
98
+ "SpanCostDetailsBySpanCostDataLoader",
99
+ "SpanCostSummaryByExperimentDataLoader",
100
+ "SpanCostSummaryByExperimentRunDataLoader",
101
+ "SpanCostSummaryByGenerativeModelDataLoader",
102
+ "SpanCostSummaryByProjectDataLoader",
103
+ "SpanCostSummaryByProjectSessionDataLoader",
104
+ "SpanCostSummaryByTraceDataLoader",
105
+ "SpanCostsDataLoader",
71
106
  "SpanDatasetExamplesDataLoader",
72
107
  "SpanDescendantsDataLoader",
73
108
  "SpanProjectsDataLoader",
@@ -76,10 +111,8 @@ __all__ = [
76
111
  "TraceByTraceIdsDataLoader",
77
112
  "TraceRetentionPolicyIdByProjectIdDataLoader",
78
113
  "TraceRootSpansDataLoader",
79
- "ProjectByNameDataLoader",
80
- "SpanAnnotationsDataLoader",
81
- "UsersDataLoader",
82
114
  "UserRolesDataLoader",
115
+ "UsersDataLoader",
83
116
  ]
84
117
 
85
118
 
@@ -103,3 +136,6 @@ class CacheForDataLoaders:
103
136
  token_count: TokenCountCache = field(
104
137
  default_factory=TokenCountCache,
105
138
  )
139
+ token_cost: SpanCostSummaryCache = field(
140
+ default_factory=SpanCostSummaryCache,
141
+ )
@@ -0,0 +1,35 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from sqlalchemy import func, select
5
+ from strawberry.dataloader import DataLoader
6
+ from typing_extensions import TypeAlias
7
+
8
+ from phoenix.db import models
9
+ from phoenix.server.types import DbSessionFactory
10
+
11
+ GenerativeModelID: TypeAlias = int
12
+ Key: TypeAlias = GenerativeModelID
13
+ Result: TypeAlias = Optional[datetime]
14
+
15
+
16
+ class LastUsedTimesByGenerativeModelIdDataLoader(DataLoader[Key, Result]):
17
+ def __init__(self, db: DbSessionFactory) -> None:
18
+ super().__init__(load_fn=self._load_fn)
19
+ self._db = db
20
+
21
+ async def _load_fn(self, keys: list[Key]) -> list[Result]:
22
+ async with self._db() as session:
23
+ last_used_times_by_model_id: dict[Key, Result] = {
24
+ model_id: last_used_time
25
+ async for model_id, last_used_time in await session.stream(
26
+ select(
27
+ models.SpanCost.model_id,
28
+ func.max(models.SpanCost.span_start_time).label("last_used_time"),
29
+ )
30
+ .select_from(models.SpanCost)
31
+ .where(models.SpanCost.model_id.in_(keys))
32
+ .group_by(models.SpanCost.model_id)
33
+ )
34
+ }
35
+ return [last_used_times_by_model_id.get(model_id) for model_id in keys]
@@ -0,0 +1,24 @@
1
+ from typing import Optional
2
+
3
+ from sqlalchemy import select
4
+ from strawberry.dataloader import DataLoader
5
+ from typing_extensions import TypeAlias
6
+
7
+ from phoenix.db import models
8
+ from phoenix.server.types import DbSessionFactory
9
+
10
+ SpanRowId: TypeAlias = int
11
+ Key: TypeAlias = SpanRowId
12
+ Result: TypeAlias = Optional[models.SpanCost]
13
+
14
+
15
+ class SpanCostBySpanDataLoader(DataLoader[Key, Result]):
16
+ def __init__(self, db: DbSessionFactory) -> None:
17
+ super().__init__(load_fn=self._load_fn)
18
+ self._db = db
19
+
20
+ async def _load_fn(self, keys: list[Key]) -> list[Result]:
21
+ stmt = select(models.SpanCost).where(models.SpanCost.span_rowid.in_(keys))
22
+ async with self._db() as session:
23
+ result = {sc.span_rowid: sc async for sc in await session.stream_scalars(stmt)}
24
+ return list(map(result.get, keys))