Harness Academy
Tài liệu gốc 31 31 phútTiếng Việt + English
docs/spend.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.

chi tiêu

Theo dõi chi phí của agent và dừng việc đó khi hết ngân sách.

Mặc dù Pydantic AI Harness có trên các bản phát hành 0.x nhưng API có thể thay đổi giữa các bản phát hành nhỏ; khi đó, cảnh báo ngừng sử dụng và hướng dẫn di chuyển ghi chú phát hành sẽ cho bạn (hoặc người đại diện của bạn) biết chính xác cách nâng cấp. Xem chính sách phiên bản.

vấn đề

Vòng lặp gọi một mô hình cho đến khi đạt được điều kiện mà nó không bao giờ đạt tới sẽ tiếp tục gọi cho đến khi có điều gì đó dừng nó lại. UsageLimits trong Pydantic AI là điểm dừng cho một lần chạy: nó giới hạn token, yêu cầu và chi phí trong suốt thời gian của một lần chạy run(). Những gì nó không bao gồm là khoảng thời gian dài hơn một lần chạy, phần chia sẻ trợ cấp chung cho mỗi người thuê hoặc bộ đếm mà một số quy trình của nhân viên đồng ý. Mức trần hàng ngày trải rộng cho các công nhân của hàng đợi chính xác là trường hợp mỗi công nhân tin rằng mình có toàn bộ ngân sách.

API sử dụng của nhà cung cấp không thu hẹp khoảng cách đó. Chúng là các quy trình thanh toán và khả năng quan sát: mức sử dụng được tổng hợp sau khi thực tế và được đọc bằng cách bỏ phiếu, do đó, một con số ở đó chỉ di chuyển sau khi các yêu cầu đằng sau nó đã được thực hiện. Điều đó là đủ để đối chiếu một sổ cái và không đủ để từ chối yêu cầu mà một vòng lặp chạy trốn sắp thực hiện.

Giải pháp

SpendLimits định giá mọi phản hồi của mô hình bằng ModelResponse.cost(), thêm nó vào mỗi cửa sổ mà bạn định cấu hình và từ chối yêu cầu tiếp theo sau khi sử dụng hết một cửa sổ.

from decimal import Decimal

from pydantic_ai import Agent
from pydantic_ai_harness import SpendLimits
from pydantic_ai_harness.spend import Budget

agent = Agent(
    'openai:gpt-5.4',
    capabilities=[SpendLimits(budgets=[Budget(usd=Decimal('100'), window='day')])],
)

Quá 100 đô la trong một ngày UTC, yêu cầu tiếp theo sẽ tăng lên SpendLimitExceeded.

Ngân sách

Ngân sách là mức trần, một khoảng thời gian và tùy chọn là một phân vùng. Họ sáng tác nên nhiều người nộp đơn cùng một lúc:

from decimal import Decimal

from pydantic_ai_harness import SpendLimits
from pydantic_ai_harness.spend import Budget

SpendLimits(
    budgets=[
        Budget(usd=Decimal('5'), window='run'),  # one runaway run
        Budget(usd=Decimal('100'), window='day'),  # the whole deployment, per day
        Budget(usd=Decimal('2000'), window='month', warn_at=0.8),
        Budget(usd=Decimal('10'), window='day', scope=lambda ctx: ctx.deps.tenant_id, name='tenant'),
    ]
)
Lĩnh vựcÝ nghĩa
usd / tokenstrần nhà; đặt một trong hai, cả hai hoặc không đặt
windowrun, conversation, day, month, total
scopelấy khóa phân vùng từ quá trình chạy, do đó người thuê sẽ tính riêng; gõ chống lại agent deps
warn_atphân số qua đó BudgetStatus.warning được thiết lập; không bao giờ chặn
namephân biệt ngân sách chia sẻ một cửa sổ và phạm vi
retainbộ đếm được giữ trong bao lâu sau lần ghi cuối cùng của nó; 'window default', 'forever', hoặc một timedelta

Một cửa sổ sẽ cuộn qua bằng cách tạo một khóa cửa hàng khác thay vì đặt lại bộ đếm, vì vậy một ngày mới chỉ đơn giản là một khóa mới và không có gì phải chạy vào lúc nửa đêm. A total bộ đếm không bao giờ hết hạn. run và conversation các thùng cũng không bao giờ lăn qua, do đó, thời hạn sử dụng sẽ quay trở lại mức trần thay vì bắt đầu một giai đoạn mới -- nhưng mỗi thùng đúc một chìa khóa cho mỗi lần chạy hoặc mỗi cuộc trò chuyện, vì vậy, chúng mang theo một khoảng thời gian dài (24 giờ và 30 ngày), qua đó bộ đếm sẽ bị hủy thay vì được giữ mãi mãi. Mặc định đó là một sự thỏa hiệp và nó có thể nhìn thấy được: một cuộc trò chuyện được tiếp tục lại sau đường chân trời của nó lại bắt đầu từ con số 0, vì vậy hãy đặt retain='forever' nơi trần cuộc trò chuyện phải kéo dài chừng nào cuộc trò chuyện diễn ra và dọn dẹp các phím theo cách khác.

Ngân sách chia sẻ một name, windowvà scope chia sẻ một bộ đếm, đó là cách một cửa sổ mang cả USD và trần token. Phản hồi được thêm vào bộ đếm đó một lần, không phải một lần cho mỗi ngân sách. Hai ngân sách có chung một name và window nhưng tuyên bố khác scope các lệnh gọi được bị từ chối khi xây dựng: chúng có các kích thước khác nhau -- chẳng hạn như đối với mỗi người thuê và mỗi người dùng -- và không có gì ngăn cản cả hai trả về cùng một chuỗi, điều này sẽ hợp nhất chúng thành một bộ đếm. Đặt cho chúng các tên khác nhau hoặc chuyển cùng một lệnh gọi cho cả hai. Các ngân sách có chung một bộ đếm cũng phải thống nhất về retain: một bộ đếm có một thời hạn sử dụng và bộ tích lũy ghi bất kỳ bộ đếm nào trong số chúng được liệt kê đầu tiên, vì vậy việc không đồng ý sẽ để lệnh khai báo quyết định khi nào một 'forever' trần nhà cuộn qua.

Budget là chung trong loại phụ thuộc của agent, vì vậy scope được kiểm tra dựa trên nó: chuyển khả năng cho một Agent với một deps_type và phạm vi tiếp cận một trường mà những người phụ trách không có là lỗi loại chứ không phải là lỗi AttributeError theo yêu cầu đầu tiên.

Ngân sách không có mức trần là một bộ đếm. Nó tích lũy, báo cáo và không bao giờ từ chối bất cứ điều gì, đó là cách thể hiện kế toán theo từng người thuê không có giới hạn:

from pydantic_ai_harness import SpendLimits
from pydantic_ai_harness.spend import Budget

SpendLimits(budgets=[Budget(window='month', scope=lambda ctx: ctx.deps.tenant_id, name='chargeback')])

Những gì cổng đảm bảo

Không có yêu cầu nào bắt đầu sau khi hết ngân sách.

Không: chi tiêu đó vẫn ở dưới mức trần. Yêu cầu vượt qua dòng hoàn tất và mỗi lần chạy đồng thời có thể vượt qua kiểm tra trước khi bất kỳ yêu cầu nào trong số chúng ghi lại bất cứ điều gì. Ba lỗ hổng nữa đáng để biết hơn là khám phá: một luồng mà người gọi bỏ qua giữa chừng sẽ không bao giờ đến được móc kế toán, do đó, token của nó sẽ được nhà cung cấp lập hóa đơn và không hiển thị ở đây; khả năng trả lời từ bộ đệm mà không gọi nhà cung cấp sẽ bị tính phí đăng ký cho phản hồi mà nó trả về; và một chuỗi tiếp nối (Anthropic pause_turn, chế độ nền OpenAI) xuất hiện dưới dạng một phản hồi hợp nhất, đây là điều mà Pydantic AI cũng tính là một yêu cầu, do đó, các phân đoạn của nó được định giá theo mức sử dụng tổng hợp thay vì từng yêu cầu một -- sự khác biệt chỉ cho thấy mức giá được xếp theo bậc chứ không phải tuyến tính. Hãy coi đây như một cú phanh cho một vòng lặp chạy trốn, chứ không phải như một cuốn sổ cái kế toán; đối chiếu với số lượng riêng của nhà cung cấp nếu bạn cần điều thứ hai.

Đọc các con số

from decimal import Decimal

from pydantic_ai_harness import SpendLimits
from pydantic_ai_harness.spend import Budget, SpendSnapshot


def show(snapshot: SpendSnapshot) -> None:
    print(f'{snapshot.model} cost ${snapshot.usd}')
    for status in snapshot.budgets:
        print(f'  {status.budget.name}: ${status.remaining_usd} left')


SpendLimits(budgets=[Budget(usd=Decimal('100'))], on_spend=show)

on_spend kích hoạt sau mỗi phản hồi, đồng bộ hóa hoặc không đồng bộ, với SpendSnapshot -- bao gồm một cái đó on_unpriced='raise' sắp bị từ chối, vì một báo cáo bỏ qua chính xác các câu trả lời chưa được định giá sẽ thiếu những câu trả lời đáng để biết. Nó mang theo câu trả lời usage không thay đổi, do đó, việc đọc và ghi bộ đệm có sẵn mà không cần khả năng lập mô hình chúng.

status() đọc các số giống nhau mà không cần chạy, đó là điều mà hiển thị chi phí trong giao diện người dùng mong muốn:

from pydantic_ai_harness import SpendLimits


async def report(limits: SpendLimits[None]) -> None:
    for status in await limits.status(scope='acme'):
        print(status.budget.name, status.spent.usd, status.exhausted)

Nếu không có bối cảnh chạy, ngân sách trên một run hoặc conversation cửa sổ bị bỏ qua và ngân sách cũng tuyên bố scope trừ khi scope= đặt tên cho phân vùng để đọc. Vượt qua ctx trong một lần chạy và mọi ngân sách đều được giải quyết.

Đặt expose_tools=True để cung cấp cho agent một get_spend công cụ. Theo mặc định, nó bị tắt: một công cụ tính phí token lược đồ cho mọi yêu cầu và hầu hết các ứng dụng đều muốn số trên màn hình thay vì trong ngữ cảnh của mô hình.

Phản ứng với một ngưỡng

on_spend đang được chờ đợi bên trong wrap_model_request, do đó, lệnh gọi lại không đồng bộ sẽ tiếp tục hoạt động ở đó. Đây vẫn là một nơi không phù hợp để yêu cầu phê duyệt: nó kích hoạt sau mỗi phản hồi đạt đến mức tích lũy, bao gồm cả phản hồi mang câu trả lời cuối cùng và SpendSnapshot không nói gì về việc liệu một lượt khác có tiếp theo hay không -- do đó, lệnh gọi lại chờ ở đó sẽ để lại một lượt chạy đã kết thúc việc chờ quyết định sẽ không có hành động nào được thực hiện. sử dụng on_spend để báo cáo.

Đường nối chạy trước một yêu cầu thay vì sau phản hồi là before_model_request. Một khả năng nhỏ của riêng bạn có thể đọc status(ctx) ở đó và tiếp tục chạy cho đến khi có người quyết định:

import asyncio
from dataclasses import dataclass
from decimal import Decimal

from pydantic_ai import Agent
from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.models import ModelRequestContext
from pydantic_ai.tools import RunContext

from pydantic_ai_harness import SpendLimits
from pydantic_ai_harness.spend import Budget

limits = SpendLimits[None](budgets=[Budget(usd=Decimal('100'), warn_at=0.8)])
approvals: asyncio.Queue[bool] = asyncio.Queue()


@dataclass
class ApproveBeforeSpending(AbstractCapability[None]):
    async def before_model_request(
        self, ctx: RunContext[None], request_context: ModelRequestContext
    ) -> ModelRequestContext:
        if any(status.warning for status in await limits.status(ctx)) and not await approvals.get():
            raise RuntimeError('spending past the warning threshold was not approved')
        return request_context


agent = Agent('openai:gpt-5.4', deps_type=type(None), capabilities=[limits, ApproveBeforeSpending()])

Cổng đọc số SpendLimits đã được tích lũy vì phản hồi trước đó đã được tính bên trong wrap_model_request trước khi yêu cầu này được chuẩn bị. Nó cũng đưa ra yêu cầu đầu tiên của một lần chạy, đó là yêu cầu mang ngưỡng được vượt qua bởi lần chạy trước đó sang lần chạy tiếp theo. Một khả năng được liệt kê sau nó vẫn có thể bỏ qua yêu cầu với SkipModelRequest, vì vậy việc phê duyệt được thực hiện ở đây không phải là bằng chứng cho thấy yêu cầu đã được thực hiện.

Việc tạm dừng đó chứa một coroutine, vì vậy nó sẽ tồn tại trong suốt thời gian quá trình thực hiện và không còn nữa. Không có sẵn tạm dừng serializable tại ranh giới yêu cầu mô hình: Đường dẫn trì hoãn của Pydantic AI chỉ là ranh giới công cụ. CallDeferred và ApprovalRequired được vinh danh khi lệnh gọi công cụ được xác thực hoặc thực thi; được nâng lên từ một hook yêu cầu mô hình, không có gì bắt được chúng và quá trình chạy kết thúc với ngoại lệ trần, không mang thông điệp riêng của nó. #151 theo dõi một ngắt chung với sự tiếp tục có thể tuần tự hóa.

Đối với một trần nhà mở rộng hơn là dừng lại, budgets được đọc mới theo mọi yêu cầu, vì vậy việc thay thế nó sau khi bị từ chối sẽ cho phép công việc tiếp tục với trần nhà lớn hơn. Bộ đếm được bật name, window, scope và khoảng thời gian mà cửa sổ hiện đang tồn tại, không bao giờ ở mức trần, vì vậy những gì đã chi tiêu sẽ được chuyển tiếp.

Việc từ chối có thể xảy ra giữa chừng, sau khi các lệnh gọi công cụ đã chạy và được thanh toán. Việc chạy lại lời nhắc ban đầu sẽ lặp lại công việc đó và mọi tác dụng phụ mà nó có, vì vậy, hãy tiếp tục từ những gì lần chạy bị từ chối đã tạo ra: capture_run_messages giữ một phần lịch sử và một lần chạy dựa trên lịch sử đó và không có lời nhắc mới nào tiếp tục từ yêu cầu bị từ chối.

import dataclasses
from decimal import Decimal

from pydantic_ai import Agent, capture_run_messages

from pydantic_ai_harness import SpendLimits
from pydantic_ai_harness.spend import Budget, SpendLimitExceeded

limits = SpendLimits[None](budgets=[Budget(usd=Decimal('1'), name='daily')])
agent = Agent('openai:gpt-5.4', deps_type=type(None), capabilities=[limits])


async def ask(prompt: str, ceiling: Decimal) -> str:
    with capture_run_messages() as messages:
        try:
            return (await agent.run(prompt)).output
        except SpendLimitExceeded:
            (budget,) = limits.budgets
            limits.budgets = [dataclasses.replace(budget, usd=ceiling)]
            resumable = list(messages)
    return (await agent.run(message_history=resumable)).output

Phân công budgets không lặp lại các bước kiểm tra mà hàm tạo chạy trên các kết hợp ngân sách, vì vậy hãy giữ một bản thay thế cho cùng tên, cửa sổ và phạm vi. Nó cũng tăng mức trần cho mỗi lần chạy chia sẻ điều này SpendLimits, không chỉ cái bị từ chối, và không có gì hạ nó xuống nữa.

Chia sẻ bộ đếm giữa các tiến trình

Cửa hàng mặc định giữ các bộ đếm trong quá trình này, bộ đếm này sẽ bắt được một vòng lặp chạy bên trong một nhân viên và không làm gì đối với ngân sách trải rộng trên một hàng đợi. RedisSpendStore là bộ đếm được chia sẻ:

from decimal import Decimal

from redis.asyncio import Redis

from pydantic_ai_harness import SpendLimits
from pydantic_ai_harness.spend import Budget, RedisSpendStore

store = RedisSpendStore(Redis.from_url('redis://localhost'))
limits = SpendLimits(budgets=[Budget(usd=Decimal('100'), window='day')], store=store)

Nó không thêm phụ thuộc: RedisClient là một giao thức của hai coroutine được sử dụng, vì vậy bất kỳ máy khách tương thích nào cũng đáp ứng được nó. Số tiền được lưu trữ dưới dạng số nguyên phần tỷ của đô la thay vì thông qua INCRBYFLOAT, tích lũy lỗi làm tròn trong hàng chục nghìn yêu cầu mà một ngày bận rộn tạo ra. Phần tỷ chứ không phải phần triệu vì phần dư không tính trung bình: một agent lặp lại các yêu cầu có hình dạng gần giống nhau, do đó, cùng một phần được làm tròn theo cùng một cách mỗi lần.

Mỗi cửa sổ mà phản hồi được tính sẽ được áp dụng dưới dạng một tập lệnh Lua, vì vậy không có khách hàng nào khác nhìn thấy phản hồi được áp dụng một phần: không phải trên bốn bộ đếm của một cửa sổ và không phải trên chính các cửa sổ. Phản hồi tính theo ngân sách ngày và ngân sách tháng là một tập lệnh chứ không phải hai tập lệnh, do đó, giữa chúng không có trường hợp nào phân biệt ngày được tính và tháng không được tính. Một ngoại lệ là lỗi tràn bên dưới: nó hủy bỏ tập lệnh ở nơi nó xảy ra và để lại các cửa sổ đã được áp dụng, khiến bộ đếm phải đạt gần 9,22 tỷ USD. Mỗi khóa cũng phải đọc lần thứ hai miễn là có sẵn dự phòng tương thích bên dưới.

Lỗi sau khi máy chủ chạy tập lệnh không cho biết tập lệnh đó có được xác nhận hay không -- kết nối có thể bị ngắt một lần EVAL đã hạ cánh -- do đó, việc viết có lỗi sẽ để lại kết quả không xác định thay vì không được thử. Không có gì thử lại: việc đếm một phản hồi được tính phí hai lần là hướng mà phanh tồn tại và việc đếm nó bằng 0 lần thì không.

Các quầy không tròn. HINCRBY là số học số nguyên 64 bit và lấy số gia của nó dưới dạng một chuỗi, do đó, những gì Redis giữ là chính xác và tổng số sẽ trả về dưới dạng chuỗi hàng loạt được đọc với HMGET thay vì trả lời số nguyên HINCRBY trả về -- những số đó trở thành số Lua, gấp đôi và sẽ làm tròn tổng số quá khứ 2**53 phần tỷ trên đường ra ngoài. Những gì còn lại là HINCRBYphạm vi riêng của: một bộ đếm vượt qua phạm vi 64 bit đã ký, khoảng 9,22 tỷ đô la đối với một khóa duy nhất, điều mà Redis từ chối trước khi viết trường đó.

Chìa khóa là {prefix}:budget-key, với các dấu ngoặc nhọn bao quanh tiền tố. Đó là thẻ băm Redis Cluster, do đó, vị trí chỉ xuất phát từ tiền tố và mọi khóa của một cửa hàng sẽ nằm trong cùng một vị trí, điều này cho phép một tập lệnh lấy một vài trong số chúng. Cái giá phải trả là một cụm không thể trải rộng các khóa của cửa hàng trên các nút của nó và prefix của riêng bạn sẽ bị từ chối khi xây dựng nếu nó trống hoặc có một dấu ngoặc nhọn riêng, một trong hai điều này sẽ ngăn thẻ được đọc dưới dạng một. BudgetStatus.key báo cáo khóa ngân sách không có tiền tố nên nội dung nó hiển thị không thay đổi. Các điểm đánh dấu khấu trừ bên dưới chia sẻ không gian tên đó, do đó, phần mở đầu của khóa dedup| bị từ chối: nó sẽ đặt tên cho một điểm đánh dấu chứa một chuỗi và bộ đếm sẽ thất bại với WRONGTYPE hơn là tích lũy. Không có ngân sách nào tạo ra một khóa như vậy -- đoạn thứ hai của một khóa luôn là một cửa sổ -- vì vậy, khóa này chỉ tiếp cận được người gọi đang lái xe đến cửa hàng.

Bộ đếm được viết bởi bản phát hành trước đó, dưới tên không được gắn thẻ, được đọc cùng với bộ đếm được gắn thẻ và được thêm vào đó, vì vậy việc nâng cấp không cần bước di chuyển. Đã thêm thay vì di chuyển: một động thái sẽ phải quyết định thời điểm hoàn thành và không có gì ở đây có thể biết điều đó, vì một nhân viên vẫn còn trên bản phát hành cũ có thể ghi vào tên cũ tại bất kỳ thời điểm nào trong quá trình triển khai luân phiên và một động thái đã chạy sẽ không bao giờ nhận được điều đó. Chi phí là một lần đọc thêm cho mỗi khóa, đối với các lần đọc và ghi giống nhau.

Khả năng tương thích chỉ diễn ra theo một chiều, đáng để lập kế hoạch dựa trên ba khía cạnh. Một triển khai luân phiên bị đếm dưới mức trong khi nó kéo dài: một nhân viên được nâng cấp nhìn thấy cả hai tên, nhưng một tên vẫn còn trên bản phát hành cũ chỉ đọc tên không được gắn thẻ và không thể xem những gì nhân viên được nâng cấp đã viết, vì vậy nó thừa nhận các yêu cầu đối với tổng số bị thiếu. hạ cấp sẽ mất hoàn toàn bộ đếm được gắn thẻ vì lý do tương tự và bản phát hành này không bao giờ ghi tên cũ nên không có gì mang lại. Và thời hạn sử dụng của khóa cũ bị đóng băng ở bất kỳ tập hợp ghi nào của bản phát hành cũ cuối cùng: không có gì ở đây làm mới nó, vì vậy nó sẽ tiếp tục khi nó hoạt động và tổng số giảm xuống theo những gì nó giữ. Giữ quá trình triển khai ngắn gọn và coi việc hạ cấp là một sự thiết lập lại thay vì một sự khôi phục.

Dự phòng sẽ biến mất sau 0.28.0. Bộ đếm vẫn tồn tại dưới tên cũ sẽ ngừng được tính tại thời điểm đó, do đó, một cửa sổ được đặt thành retain='forever', hoặc một đường chân trời có đường chân trời vượt xa khoảng cách giữa hai bản phát hành, đáng để di chuyển bằng tay trước đó.

add_many mang theo token xác định phản hồi và RedisSpendStore đọc điểm đánh dấu cho nó trước số gia và viết nó sau chúng, bên trong cùng một tập lệnh. InMemorySpendStore ghi nhớ các token giống nhau dưới khóa của nó, vì vậy cửa hàng mặc định hoạt động theo cùng một cách. Một công cụ bền bỉ thực hiện lại việc tích lũy xung quanh phản hồi mà nó đã giữ -- DBOS khôi phục quy trình làm việc, thử lại luồng Prefect -- do đó thêm nó một lần thay vì hai lần. token là id phản hồi của chính nhà cung cấp nơi nhà cung cấp báo cáo; không có nó, nó sẽ quay trở lại run id, bước và dấu thời gian phản hồi, chỉ tồn tại khi phát lại trong một run_id người gọi đã chọn thay vì mặc định được tạo. Điểm đánh dấu được giữ cho dedup_retain, một trường trên cả hai cửa hàng, một giờ theo mặc định hoặc cho khoảng thời gian riêng của cửa sổ nơi thời gian đó ngắn hơn và tốn một khóa nhỏ cho mỗi phản hồi trên mỗi cửa sổ. Đường chân trời đó là khoảng thời gian mà lượt phát lại được ghi nhận, chứ không phải thời gian tồn tại của bộ đếm: một phản hồi được phát lại muộn hơn thời gian nó được đếm lại, đó là hướng có thể xảy ra sai sót, vì phanh đi sớm vẫn tồn tại còn phanh nhả muộn thì không. Đặt dedup_retain=None không giữ điểm đánh dấu và áp dụng mọi mục nhập, điều này cũng từ bỏ sự đảm bảo. Việc ghi nhận bắt đầu khi nâng cấp theo một trong hai cách: phản hồi mà bản phát hành trước đó được tính không để lại điểm đánh dấu nào, do đó, lần phát lại bản phát hành đó sẽ được tính lại.

Cửa hàng mặc định được xây dựng theo khả năng, vì vậy hai SpendLimits các trường hợp không lặng lẽ chia sẻ một bộ đếm. Chuyển cùng một đối tượng cửa hàng cho cả hai khi bạn muốn.

Một cửa hàng thất bại không thất bại một cách lặng lẽ. Lỗi đọc bộ đếm từ chối yêu cầu, đó là hướng an toàn. Lỗi ghi nó sẽ lan ra ngoài quá trình chạy sau khi mô hình đã trả lời và được sạc. Đó là cố ý: việc nuốt chữ sẽ làm trôi bộ đếm xuống và làm suy yếu cổng, điều này còn tệ hơn cả lỗi có thể nhìn thấy được. Nếu việc triển khai của bạn muốn giữ câu trả lời hơn là số lượng, hãy đóng gói cửa hàng và quyết định ở đó.

Bất kỳ đồ vật nào có get_many và add_many hoạt động nên bộ đếm Postgres hoặc DynamoDB là một lớp nhỏ chứ không phải là một nhánh. Bốn nghĩa vụ đi kèm với việc viết một. Trả về tổng số cho mỗi chìa khóa bạn đã được giao, được khóa bởi SpendEntry.key, bao gồm cả phần bạn đã bỏ qua khi phát lại: SpendLimits lập chỉ mục kết quả theo khóa, vì vậy kết quả bị thiếu là KeyError một phần của cuộc chạy. Đọc một khóa không bao giờ được viết bằng 0 thay vì bỏ nó đi. Bỏ qua một mục có token đã được áp dụng cho khóa đó hoặc việc tích lũy lại động cơ bền bỉ sẽ được tính hai lần. Và áp dụng toàn bộ cuộc gọi hoặc không áp dụng cuộc gọi nào -- sự đảm bảo ở đầu phần này chỉ tốt như phần phụ trợ đằng sau nó và một cửa hàng cam kết mỗi mục nhập khi nó quay trở lại phần ghi phân tách mà đường nối này tồn tại để xóa. Cả hai phương thức đều không bao giờ được trao một chuỗi trống, vì vậy không có trường hợp nào để trả lời.

SpendStore, phím đơn get và add cặp được phát hành trong 0.17.0, không được dùng nữa và bị xóa trong 0.28.0: một cửa hàng có hình dạng đó vẫn hoạt động, điều khiển một cửa sổ cho mỗi cuộc gọi và phát ra một cửa sổ HarnessDeprecationWarning khi SpendLimits việc giữ nó được xây dựng, đặt tên cho chi phí đó -- các cửa sổ được áp dụng lần lượt và không có token nào để nhận ra lần lặp lại. Phím đơn get và add trên InMemorySpendStore và RedisSpendStore đi vào bản phát hành đó quá. Gọi trực tiếp tới một trong hai đều không cảnh báo nên đây là thông báo; đạt tới get_many và add_many thay vào đó. Một lớp con ghi đè một trong số chúng mà không ghi đè cặp lô sẽ được cảnh báo khi chính cửa hàng được xây dựng, vì trường hợp đó sẽ mất hành vi thay vì chỉ đặt tên cho một phương thức không được dùng nữa: SpendLimits ổ đĩa get_many và add_many, do đó ghi đè lên get hoặc add không bao giờ được gọi và bất cứ điều gì nó thêm vào -- một cuộc kiểm tra, một bản ghi phản chiếu -- sẽ ngừng diễn ra. Di chuyển nó lên get_many hoặc add_many, đó cũng là nguyên nhân khiến cảnh báo dừng lại.

Định giá

Giá đến từ genai-price thông qua ModelResponse.cost(), trên mỗi phản hồi: giá bộ nhớ đệm và cấp được tính theo yêu cầu, do đó, việc tính tổng mức sử dụng trên các yêu cầu và định giá tổng sẽ đưa ra con số sai.

Một mô hình mà cơ quan đăng ký không biết -- triển khai cục bộ, tỷ lệ thương lượng -- được xử lý bởi price:

from decimal import Decimal

from pydantic_ai_harness import SpendLimits

SpendLimits(price=lambda response: Decimal('0.002') if response.model_name == 'internal-7b' else None)

Một số tiền được trả lại bởi price phải hữu hạn và không âm. Bất cứ điều gì khác -- một khoản tín dụng, một NaN, một vô cực -- không chạy được với UserError, bởi vì tín dụng sẽ đẩy ngân sách ra xa mức trần của nó và hai khoản còn lại là một hàm định giá bị hỏng chứ không phải là một mức giá. Phản hồi vẫn được ghi lại trước tiên: nó được nhà cung cấp lập hóa đơn cho dù chức năng trả về có như thế nào, do đó, token và số lượng yêu cầu của nó sẽ được tích lũy và on_spend kích hoạt trước khi lỗi được nêu ra.

Trở về None rơi vào sổ đăng ký. Khi không gì có thể định giá được một phản ứng, on_unpriced quyết định: 'zero' (mặc định) tính nó là miễn phí và tăng dần Spent.unpriced_requests vì vậy khoảng cách có thể nhìn thấy được, và 'raise' thất bại trong việc chạy với UnpricedModelError. Dù bằng cách nào, phản hồi sẽ được ghi lại trước tiên và số token được tính, do đó, mức trần token vẫn được giữ cho một mô hình không có giá và ứng dụng phát hiện lỗi sẽ không tiếp tục đối với bộ đếm được đánh giá thấp. Dưới 'zero' mức trần USD là mức không thể giữ được: không có gì có giá trị tích lũy, vì vậy không có số lượng yêu cầu nào đạt được mức đó. Sự kết hợp đó -- 'zero' cộng với một usd ngân sách -- cảnh báo một lần cho mỗi mô hình với UnpricedModelWarning, thay vì một lần cho mỗi yêu cầu. Nếu người gọi chọn kiểu máy, ưu tiên 'raise' hoặc cung cấp price.

Thành phần

Bang sống qua chạy cố tình, vì vậy for_run không bị ghi đè: ngân sách hàng ngày đặt lại mỗi lần chạy sẽ không phải là ngân sách hàng ngày. Sự cô lập mỗi lần chạy đến từ Budget(window='run'), khóa của nó mang id chạy.

defer_loading=True bị từ chối. Các hook của khả năng bị trì hoãn không chạy cho đến khi mô hình tải nó, do đó, ngân sách cạn kiệt sẽ không dừng yêu cầu và các yêu cầu được đưa ra trong khi đó sẽ không được tính -- một phanh mà thứ bị hãm sẽ quyết định thời điểm áp dụng.

Sự tích lũy xảy ra trong wrap_model_request, ngay lập tức xung quanh lệnh gọi của nhà cung cấp và khả năng tự khai báo trong cùng để trình bao bọc đó nằm bên trong mọi khả năng bên ngoài tầng trong cùng. Mỗi after_model_request chạy bên ngoài nó và mọi trình bao bọc cũng vậy, ngoại trừ khả năng cấp trong cùng được liệt kê sau nó.

after_model_request là cái móc sai cho việc này. Nó chạy khi toàn bộ chuỗi bọc đã quay trở lại, do đó, một khả năng riêng wrap_model_request chờ đợi phản hồi và sau đó tăng lên ModelRetry gửi yêu cầu thẳng đến yêu cầu mới và yêu cầu bị từ chối -- được tạo, lập hóa đơn, lưu trong lịch sử -- không bao giờ được tính. Thứ tự không thể đạt được trường hợp đó: khả năng từ chối không cần phải ở trong cùng và một được liệt kê trước SpendLimits vẫn bao bọc bên ngoài nó.

Gói cũng có nghĩa là yêu cầu mà nhà cung cấp chưa từng thấy sẽ không bị tính phí. SkipModelRequest từ một khả năng trước đó before_model_request đạt tới after_model_request với một phản hồi mà lần chạy đó không bao giờ được trả tiền nhưng cũng không bao giờ đến được trình xử lý được bao bọc.

Những gì còn lại là anh chị em. AI Pydantic ra lệnh cho các khả năng trong cùng chỉ chống lại những khả năng không phải trong cùng và trong số chúng, những khả năng được liệt kê sau sẽ ẩn sâu hơn trong đó. InputGuardrail và TemporalDurability cũng tuyên bố mình là người ở trong cùng, nên được liệt kê sau SpendLimits quấn vào bên trong nó. InputGuardrail là người có thể từ chối phản hồi được tính phí trước khi nó được tính; TemporalDurability trả về phản hồi nguyên vẹn bên ngoài một bối cảnh lâu dài và định tuyến nó vào một hoạt động bên trong một bối cảnh, trong đó SpendLimits đã thất bại trên đồng hồ. Với InputGuardrail(parallel=True) điều quyết định là liệu người bảo vệ có chặn hay không, chứ không phải ai thắng cuộc đua: lời nhắc bị chặn không được tính trong kết quả vì người bảo vệ sẽ hủy cuộc gọi khi nó giải quyết trước và loại bỏ câu trả lời khi mô hình thực hiện. Thứ hai là số lượng thiếu -- một phản hồi mà nhà cung cấp tính phí SpendLimits không bao giờ nhìn thấy. Danh sách SpendLimits cuối cùng trong số những khả năng sâu thẳm nhất của bạn, nơi sự khác biệt đóng vai trò quan trọng. Việc đóng nó hoàn toàn cần một cách để sắp xếp các khả năng sâu nhất với nhau, được theo dõi trong #534.

SpendLimits báo cáo sự sắp xếp đó thay vì để nó đọc ở đây. Trước mỗi yêu cầu mô hình, nó đọc chuỗi đã sắp xếp từ RunContext.root_capability và cảnh báo với SpendCompositionWarning, đặt tên cho các khả năng được liệt kê sau nó mang lại wrap_model_request của riêng họ. Một sắp xếp sẽ báo cáo một lần, không phải một lần cho mỗi yêu cầu -- và đó là sắp xếp được ghi nhớ thay vì thực tế đã báo cáo, do đó, một agent có lần chạy đầu tiên an toàn vẫn được đọc trong lần chạy sau đó bổ sung trình bao bọc bên trong thông qua agent.run(capabilities=[...]). Một lời cảnh báo hơn là từ chối, và nhấn mạnh vào mệnh lệnh hơn là vào khả năng thực hiện nó. Không có điều kiện nào ở trên được đọc: parallel có thể được lật mà không di chuyển bất kỳ thứ gì trong danh sách, và cả phán quyết cũng như cuộc đua đều không được giải quyết tại thời điểm báo cáo được đưa ra. Vì vậy nó cũng đặt tên theo trình tự InputGuardrail liệt kê sau SpendLimits, tăng lên trước khi yêu cầu được thực hiện và không thể đếm thiếu. Việc sắp xếp lại sẽ khiến nó im lặng và dù sao thì đoạn văn trên cũng khuyến nghị điều đó.

Ba loại khả năng bị loại khỏi báo cáo đó. A Hooks không được đặt tên: nó định nghĩa wrap_model_request có hay không một model_request hook đã được đăng ký và sổ đăng ký có tên là riêng tư (pydantic-ai#7177). A WrapperCapability được trả lời trên bất cứ thứ gì nó gói gọn, vì chính nó wrap_model_request chỉ các đại biểu -- do đó, trình bao bọc trên trình từ chối thực sự vẫn được đặt tên. Và khả năng thực thi lâu dài -- một trong các tích hợp Temporal, DBOS và Prefect đi kèm -- bị bỏ qua vì việc sắp xếp lại không phải là cách khắc phục cũng như không có sẵn: định tuyến vào một đơn vị bền vững không từ chối những gì quay trở lại và cốt lõi yêu cầu gửi đi là trình bao bọc cuối cùng xung quanh trình xử lý mô hình, vì vậy việc liệt kê SpendLimits sau khi đó là điều mà người đọc không được làm. cái gì SpendLimits không hỗ trợ đối với một động cơ bền bỉ là một vấn đề lớn hơn việc đặt hàng và phần thực thi bền bỉ bên dưới sẽ giải quyết vấn đề đó.

Hoạt động bền bỉ. SpendLimits không được hỗ trợ trong quy trình làm việc tạm thời và trong DBOS hoặc Prefect chỉ trong phạm vi giới hạn bên dưới.

Các móc khả năng chạy trong mã điều phối, trong khi yêu cầu mô hình mà chúng bao bọc là một đơn vị lâu bền -- một hoạt động Tạm thời, một bước DBOS, một nhiệm vụ Trưởng. Cả ba đều khôi phục bằng cách thực thi lại mã điều phối, do đó, chúng thực hiện lại việc tích lũy với mã đó trong khi yêu cầu mô hình bên cạnh mã đó được khôi phục từ điểm kiểm tra thay vì chạy lại. Pydantic AI đã xử lý tác dụng phụ trong mã điều phối như một thứ cần bảo vệ: trên DBOS, agent bền bỉ chạy cấu hình của nó event_stream_handler bên trong một bước và trên Prefect bên trong một tác vụ được lưu trong bộ nhớ đệm, do đó quá trình khôi phục sẽ phát lại kết quả đã ghi thay vì gọi lại trình xử lý. Việc tích lũy không có trình bao bọc như vậy. Thay vào đó, những gì nó thực hiện khi thực hiện lại sẽ do cửa hàng quyết định và SpendEntry.token là điều khiến cửa hàng có điều gì đó để quyết định.

Điều khác biệt giữa các động cơ là liệu bạn có tìm ra hay không. Tăng tạm thời đối với yêu cầu đầu tiên: hộp cát quy trình làm việc của nó hạn chế đồng hồ treo tường mà các móc này đọc trước khi xem xét một ngân sách duy nhất, do đó, các loại cửa sổ được định cấu hình không có sự khác biệt, pydantic_ai_harness không nằm trong số các mô-đun PydanticAIPlugin đi qua hộp cát và SpendLimits dịch lỗi đó thành ý nghĩa của nó thay vì chuyển sang cài đặt khiến nó im lặng. Việc chuyển gói qua hộp cát sẽ xóa tin nhắn chứ không phải tin nhắn phát lại và việc bỏ qua thời gian trong ngày của quy trình làm việc và ngày của chìa khóa vẫn chênh lệch nhau. Quá trình thử lại luồng Prefect và quá trình khôi phục DBOS không báo cáo gì cả và những gì bộ đếm lưu giữ sau đó sẽ theo sau cửa hàng chứ không phải động cơ.

SpendEntry.token đặt tên cho phản hồi mà khoản tích lũy dành cho: tên riêng của nhà cung cấp và id phản hồi nếu có, id chạy và bước phản hồi đến nơi không có và ModelResponse.timestamp trong cả hai trường hợp -- do đó, hai phản hồi được báo cáo theo một id sẽ không thu gọn thành một điểm đánh dấu. Cửa hàng nhận ra token đã áp dụng cho khóa sẽ trả về tổng số hiện tại của khóa đó thay vì thêm lại. Vì vậy, quá trình khôi phục cửa hàng vẫn đạt -- một cửa hàng được chia sẻ hoặc cửa hàng mặc định trong quá trình khi thử lại chạy lại trong quy trình đã xây dựng nó -- tính phản hồi được phát lại một lần. Một điều nó không thể đạt được, bởi vì sự phục hồi đã đến với một công nhân mới đang cầm một chiếc máy mới InMemorySpendStore, không có bộ đếm cũng như điểm đánh dấu có thể bắt được lượt phát lại, vì vậy nó bắt đầu từ con số không và thừa nhận quá nhiều.

Những giới hạn nào được giải quyết trong phần "Chia sẻ bộ đếm giữa các quy trình" ở trên: độ trễ phát lại vẫn được nhận ra, mức độ cửa hàng đạt được thông qua phiên bản không được dùng nữa SpendStore giao thức từ bỏ và id chạy mà nhà cung cấp báo cáo không có id phản hồi để bạn chọn. Một ràng buộc thuộc về nơi này chứ không phải ở đó. Một điểm đánh dấu được khóa trên khóa cửa hàng mà phản hồi được đưa ra và khóa đó mang cửa sổ đồng hồ treo tường được đặt tên vào thời điểm tích lũy, do đó, lần phát lại đến sau nửa đêm sẽ được áp dụng cho bộ đếm của ngày hôm sau mà không có điểm đánh dấu nào che phủ. Tất cả đều mắc lỗi theo cùng một cách, và đó là trường hợp có thể sống sót: một phản hồi được tính phí được tính hai lần thay vì một lần bị loại bỏ.

Hãy từ chối quy trình tiếp nhận trước khi bắt đầu quy trình đó. Đó là lý do tại sao exhausted() hoạt động mà không có RunContext:

from collections.abc import Awaitable, Callable

from pydantic_ai_harness import SpendLimits


async def start_if_funded(
    limits: SpendLimits[None], tenant_id: str, start_workflow: Callable[[], Awaitable[object]]
) -> None:
    if await limits.exhausted(scope=tenant_id):
        raise RuntimeError('daily budget exhausted')
    await start_workflow()

exhausted thay vì any(s.exhausted for s in await limits.status(...)): status bỏ qua ngân sách mà nó không thể giải quyết được, và any() trên những gì còn lại là một cái phanh vượt qua không kiểm tra gì -- đó chính xác là những gì một SpendLimits ngân sách của họ đều có lợi nhuận trong phạm vi khi phạm vi bị thiếu. exhausted thay vào đó tăng lên, đặt tên cho các ngân sách cần scope hoặc một bối cảnh chạy. sử dụng status để đọc, exhausted cho một quyết định.

Tất cả chỉ là nhập học: séc không dự trữ gì cả, vì vậy số tiền mà quy trình làm việc tiếp tục chi tiêu là cánh cổng này không hề bị giới hạn bởi điều gì. Mức chi tiêu vượt mức đã được ghi nhận, không phải mức trần những gì tiếp theo.

Đóng những gì còn lại cần phím cửa sổ đến từ đồng hồ điều khiển động cơ và cửa hàng ghi xảy ra bên trong bộ phận bền bỉ của động cơ. Không phải thứ gì khả năng cũng có thể sắp xếp mà không phụ thuộc vào temporalio, dbos hoặc prefect và phát hiện cái nào đang chạy, vì vậy cả hai đều thuộc về lõi AI Pydantic chứ không phải ở đây. Đã theo dõi trong #531.

Đối với trần nhà bao phủ một đường chạy và không có gì khác, của riêng Pydantic AI UsageLimits thực hiện công việc tương tự đang trong quá trình không có cửa hàng và không có khả năng: total_tokens_limit cho các token và cost_limit cho tiền, cả hai chỉ trong một lần run(). Một phản hồi không được định giá sẽ không thêm gì vào RunUsage.cost, vậy cost_limit đo lường hoạt động chống lại bất kỳ phần nào của nó có thể được định giá: a CostNotFoundWarning sau cuộc chạy khi không có gì trong số đó và im lặng khi chỉ có một số trong số đó. SpendLimits đếm cùng một khoảng cách và cho phép on_unpriced quyết định phải làm gì với nó. UsageLimits cũng mang theo cả hai mức độ chi tiết của token đầu vào SpendLimits không có tương đương cho: input_tokens_limit được tích lũy trong quá trình chạy, và per_request_input_tokens_limit giới hạn một yêu cầu đối với nhà cung cấp được báo cáo token đầu vào của phản hồi đã được trả tiền cho nó. count_tokens_before_request=True đếm yêu cầu đang chờ xử lý với yêu cầu của chính mô hình count_tokens và áp dụng cả hai giới hạn cho số lượng đó trước khi gửi, do đó, bối cảnh quá lớn sẽ bị từ chối thay vì bị tính phí cho các nhà cung cấp thực hiện count_tokens; trường đặt tên cho chúng và một mô hình không có nó sẽ tăng lên NotImplementedError thay vào đó. Tiếp cận với Budget(tokens=..., window='run') khi giống nhau cấu hình cũng phải thể hiện một cửa sổ dài hơn một lần chạy, phạm vi đối tượng thuê hoặc bộ đếm được chia sẻ giữa các tiến trình.

Truy tìm

Một lời từ chối phát ra một spend budget exhausted trải dài với spend.budget và spend.window. Tích lũy không phát ra gì: một khoảng cho mỗi yêu cầu mô hình sẽ tăng gấp đôi kích thước của dấu vết mà không cần thêm quyết định. spend.scope chỉ được đính kèm khi RunContext.trace_include_content được đặt vì khóa phạm vi thường là đối tượng thuê hoặc id người dùng và dấu vết có đối tượng rộng hơn ứng dụng tạo ra nó.

Thông số kỹ thuật

Agent.from_spec hỗ trợ phần cấu hình mà thông số kỹ thuật có thể thể hiện:

- SpendLimits:
    budgets:
      - {usd: '100', window: day}
      - {usd: '2000', window: month, warn_at: 0.8}
    on_unpriced: raise

store, price, on_spend, clock, và ngân sách scope lấy vật thể gọi được hoặc vật thể sống. Một thông số kỹ thuật đặt tên cho chúng sẽ bị từ chối thay vì bị âm thầm bỏ qua, bởi vì một thông số kỹ thuật hứa hẹn phạm vi cho mỗi đối tượng thuê nhưng không cung cấp thông số đó còn tệ hơn một thông số kỹ thuật từ chối tải.

Các trường trên là những gì SpendLimits.from_spec tên trong chữ ký của nó, đây cũng là những gì Pydantic AI đọc để tạo ra lược đồ JSON của thông số kỹ thuật -- vì vậy một trình soạn thảo tuân theo $schema dòng hoàn thành và xác nhận chúng. BudgetSpec là hình dạng mục nhập, được xuất cho bất kỳ ai xây dựng thông số kỹ thuật trong mã.

Nguồn: pydantic_ai_harness/spend/.

Tham chiếu API

::: pydantic_ai_harness.spend.SpendLimits

::: pydantic_ai_harness.spend.Budget

::: pydantic_ai_harness.spend.SpendSnapshot

::: pydantic_ai_harness.spend.BudgetStatus

::: pydantic_ai_harness.spend.Spent

::: pydantic_ai_harness.spend.BatchSpendStore

::: pydantic_ai_harness.spend.SpendEntry

::: pydantic_ai_harness.spend.SpendStore

::: pydantic_ai_harness.spend.InMemorySpendStore

::: pydantic_ai_harness.spend.RedisSpendStore

::: pydantic_ai_harness.spend.SpendLimitExceeded

::: pydantic_ai_harness.spend.UnpriceModelError

::: pydantic_ai_harness.spend.UnpriceModelWarning

::: pydantic_ai_harness.spend.SpendCompositionWarning

Mục lục nguồn