mppi-protocol 0.1.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.
- mppi/__init__.py +1 -0
- mppi/py.typed +0 -0
- mppi/v1/__init__.py +1 -0
- mppi/v1/mppi_pb2.py +104 -0
- mppi/v1/mppi_pb2.pyi +515 -0
- mppi/v1/mppi_pb2_grpc.py +185 -0
- mppi_protocol/__init__.py +45 -0
- mppi_protocol/_observers.py +121 -0
- mppi_protocol/accounting.py +95 -0
- mppi_protocol/amounts.py +124 -0
- mppi_protocol/capabilities.py +79 -0
- mppi_protocol/catalog.py +206 -0
- mppi_protocol/chain.py +789 -0
- mppi_protocol/circle.py +416 -0
- mppi_protocol/contract.py +363 -0
- mppi_protocol/contract_abi.json +1167 -0
- mppi_protocol/deployment.py +83 -0
- mppi_protocol/discovery.py +233 -0
- mppi_protocol/errors.py +20 -0
- mppi_protocol/follower.py +265 -0
- mppi_protocol/http.py +104 -0
- mppi_protocol/operations.py +145 -0
- mppi_protocol/py.typed +0 -0
- mppi_protocol/registry.py +685 -0
- mppi_protocol/signing.py +318 -0
- mppi_protocol/types.py +297 -0
- mppi_protocol/wire.py +472 -0
- mppi_protocol-0.1.0.dist-info/METADATA +35 -0
- mppi_protocol-0.1.0.dist-info/RECORD +31 -0
- mppi_protocol-0.1.0.dist-info/WHEEL +4 -0
- mppi_protocol-0.1.0.dist-info/licenses/LICENSE +202 -0
mppi/v1/mppi_pb2_grpc.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from mppi.v1 import mppi_pb2 as mppi_dot_v1_dot_mppi__pb2
|
|
7
|
+
|
|
8
|
+
GRPC_GENERATED_VERSION = '1.84.0'
|
|
9
|
+
GRPC_VERSION = grpc.__version__
|
|
10
|
+
_version_not_supported = False
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from grpc._utilities import first_version_is_lower
|
|
14
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
|
15
|
+
except ImportError:
|
|
16
|
+
_version_not_supported = True
|
|
17
|
+
|
|
18
|
+
if _version_not_supported:
|
|
19
|
+
raise RuntimeError(
|
|
20
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
21
|
+
+ ' but the generated code in mppi/v1/mppi_pb2_grpc.py depends on'
|
|
22
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
23
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
24
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ChatStub:
|
|
29
|
+
"""Binary protobuf is the v1 transport. Semantic requirements, including scalar
|
|
30
|
+
presence, widths, directions and accounting rules, are in docs/protocol.md.
|
|
31
|
+
A successfully parsed message is not proof of payment authority.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, channel):
|
|
36
|
+
"""Constructor.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
channel: A grpc.Channel.
|
|
40
|
+
"""
|
|
41
|
+
self.Completions = channel.unary_stream(
|
|
42
|
+
'/mppi.v1.Chat/Completions',
|
|
43
|
+
request_serializer=mppi_dot_v1_dot_mppi__pb2.CompleteRequest.SerializeToString,
|
|
44
|
+
response_deserializer=mppi_dot_v1_dot_mppi__pb2.ChatChunk.FromString,
|
|
45
|
+
_registered_method=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ChatServicer:
|
|
49
|
+
"""Binary protobuf is the v1 transport. Semantic requirements, including scalar
|
|
50
|
+
presence, widths, directions and accounting rules, are in docs/protocol.md.
|
|
51
|
+
A successfully parsed message is not proof of payment authority.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def Completions(self, request, context):
|
|
56
|
+
"""One request, provider JSON payloads, then one definitive final record when
|
|
57
|
+
available. Cancellation and transport failure may prevent final delivery.
|
|
58
|
+
"""
|
|
59
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
60
|
+
context.set_details('Method not implemented!')
|
|
61
|
+
raise NotImplementedError('Method not implemented!')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def add_ChatServicer_to_server(servicer, server):
|
|
65
|
+
rpc_method_handlers = {
|
|
66
|
+
'Completions': grpc.unary_stream_rpc_method_handler(
|
|
67
|
+
servicer.Completions,
|
|
68
|
+
request_deserializer=mppi_dot_v1_dot_mppi__pb2.CompleteRequest.FromString,
|
|
69
|
+
response_serializer=mppi_dot_v1_dot_mppi__pb2.ChatChunk.SerializeToString,
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
generic_handler = grpc.method_handlers_generic_handler(
|
|
73
|
+
'mppi.v1.Chat', rpc_method_handlers)
|
|
74
|
+
server.add_generic_rpc_handlers((generic_handler,))
|
|
75
|
+
server.add_registered_method_handlers('mppi.v1.Chat', rpc_method_handlers)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# This class is part of an EXPERIMENTAL API.
|
|
79
|
+
class Chat:
|
|
80
|
+
"""Binary protobuf is the v1 transport. Semantic requirements, including scalar
|
|
81
|
+
presence, widths, directions and accounting rules, are in docs/protocol.md.
|
|
82
|
+
A successfully parsed message is not proof of payment authority.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def Completions(request,
|
|
88
|
+
target,
|
|
89
|
+
options=(),
|
|
90
|
+
channel_credentials=None,
|
|
91
|
+
call_credentials=None,
|
|
92
|
+
insecure=False,
|
|
93
|
+
compression=None,
|
|
94
|
+
wait_for_ready=None,
|
|
95
|
+
timeout=None,
|
|
96
|
+
metadata=None):
|
|
97
|
+
return grpc.experimental.unary_stream(
|
|
98
|
+
request,
|
|
99
|
+
target,
|
|
100
|
+
'/mppi.v1.Chat/Completions',
|
|
101
|
+
mppi_dot_v1_dot_mppi__pb2.CompleteRequest.SerializeToString,
|
|
102
|
+
mppi_dot_v1_dot_mppi__pb2.ChatChunk.FromString,
|
|
103
|
+
options,
|
|
104
|
+
channel_credentials,
|
|
105
|
+
insecure,
|
|
106
|
+
call_credentials,
|
|
107
|
+
compression,
|
|
108
|
+
wait_for_ready,
|
|
109
|
+
timeout,
|
|
110
|
+
metadata,
|
|
111
|
+
_registered_method=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ControlStub:
|
|
115
|
+
"""Missing associated documentation comment in .proto file."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, channel):
|
|
118
|
+
"""Constructor.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
channel: A grpc.Channel.
|
|
122
|
+
"""
|
|
123
|
+
self.Session = channel.stream_stream(
|
|
124
|
+
'/mppi.v1.Control/Session',
|
|
125
|
+
request_serializer=mppi_dot_v1_dot_mppi__pb2.ControlMessage.SerializeToString,
|
|
126
|
+
response_deserializer=mppi_dot_v1_dot_mppi__pb2.ControlMessage.FromString,
|
|
127
|
+
_registered_method=True)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ControlServicer:
|
|
131
|
+
"""Missing associated documentation comment in .proto file."""
|
|
132
|
+
|
|
133
|
+
def Session(self, request_iterator, context):
|
|
134
|
+
"""The actual authenticated stream owns one session attachment. No prompts or
|
|
135
|
+
inference output travel on this stream. Vouchers have no per-message ACK.
|
|
136
|
+
"""
|
|
137
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
138
|
+
context.set_details('Method not implemented!')
|
|
139
|
+
raise NotImplementedError('Method not implemented!')
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def add_ControlServicer_to_server(servicer, server):
|
|
143
|
+
rpc_method_handlers = {
|
|
144
|
+
'Session': grpc.stream_stream_rpc_method_handler(
|
|
145
|
+
servicer.Session,
|
|
146
|
+
request_deserializer=mppi_dot_v1_dot_mppi__pb2.ControlMessage.FromString,
|
|
147
|
+
response_serializer=mppi_dot_v1_dot_mppi__pb2.ControlMessage.SerializeToString,
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
generic_handler = grpc.method_handlers_generic_handler(
|
|
151
|
+
'mppi.v1.Control', rpc_method_handlers)
|
|
152
|
+
server.add_generic_rpc_handlers((generic_handler,))
|
|
153
|
+
server.add_registered_method_handlers('mppi.v1.Control', rpc_method_handlers)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# This class is part of an EXPERIMENTAL API.
|
|
157
|
+
class Control:
|
|
158
|
+
"""Missing associated documentation comment in .proto file."""
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def Session(request_iterator,
|
|
162
|
+
target,
|
|
163
|
+
options=(),
|
|
164
|
+
channel_credentials=None,
|
|
165
|
+
call_credentials=None,
|
|
166
|
+
insecure=False,
|
|
167
|
+
compression=None,
|
|
168
|
+
wait_for_ready=None,
|
|
169
|
+
timeout=None,
|
|
170
|
+
metadata=None):
|
|
171
|
+
return grpc.experimental.stream_stream(
|
|
172
|
+
request_iterator,
|
|
173
|
+
target,
|
|
174
|
+
'/mppi.v1.Control/Session',
|
|
175
|
+
mppi_dot_v1_dot_mppi__pb2.ControlMessage.SerializeToString,
|
|
176
|
+
mppi_dot_v1_dot_mppi__pb2.ControlMessage.FromString,
|
|
177
|
+
options,
|
|
178
|
+
channel_credentials,
|
|
179
|
+
insecure,
|
|
180
|
+
call_credentials,
|
|
181
|
+
compression,
|
|
182
|
+
wait_for_ready,
|
|
183
|
+
timeout,
|
|
184
|
+
metadata,
|
|
185
|
+
_registered_method=True)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Shared MPPI primitives; runtime boundaries live in dedicated typed modules."""
|
|
2
|
+
|
|
3
|
+
from .accounting import AccountingTransition, RunAccounting, RunStatus, report_transition
|
|
4
|
+
from .amounts import (
|
|
5
|
+
MAX_UINT64,
|
|
6
|
+
RATE_SCALE,
|
|
7
|
+
Rates,
|
|
8
|
+
TokenCounts,
|
|
9
|
+
cumulative_usage,
|
|
10
|
+
decimal_uint64,
|
|
11
|
+
funding_amount,
|
|
12
|
+
priced_numerator,
|
|
13
|
+
remaining_funding_tokens,
|
|
14
|
+
required_coverage,
|
|
15
|
+
top_up_total,
|
|
16
|
+
uint64,
|
|
17
|
+
)
|
|
18
|
+
from .catalog import Catalog, CatalogRow, parse_catalog, publish_catalog, validate_model
|
|
19
|
+
from .errors import ValidationCode, ValidationError
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"MAX_UINT64",
|
|
23
|
+
"RATE_SCALE",
|
|
24
|
+
"AccountingTransition",
|
|
25
|
+
"Catalog",
|
|
26
|
+
"CatalogRow",
|
|
27
|
+
"Rates",
|
|
28
|
+
"RunAccounting",
|
|
29
|
+
"RunStatus",
|
|
30
|
+
"TokenCounts",
|
|
31
|
+
"ValidationCode",
|
|
32
|
+
"ValidationError",
|
|
33
|
+
"cumulative_usage",
|
|
34
|
+
"decimal_uint64",
|
|
35
|
+
"funding_amount",
|
|
36
|
+
"parse_catalog",
|
|
37
|
+
"priced_numerator",
|
|
38
|
+
"publish_catalog",
|
|
39
|
+
"remaining_funding_tokens",
|
|
40
|
+
"report_transition",
|
|
41
|
+
"required_coverage",
|
|
42
|
+
"top_up_total",
|
|
43
|
+
"uint64",
|
|
44
|
+
"validate_model",
|
|
45
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Bounded, best-effort lifecycle observers; never a payment control path."""
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from dataclasses import dataclass, fields, is_dataclass
|
|
7
|
+
from inspect import iscoroutinefunction
|
|
8
|
+
from threading import Condition, Thread
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class ObserverStats:
|
|
14
|
+
delivered: int
|
|
15
|
+
dropped: int
|
|
16
|
+
failed: int
|
|
17
|
+
queued: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class HookError:
|
|
22
|
+
"""Sanitized SDK boundary error; no prompt, credentials or upstream body."""
|
|
23
|
+
|
|
24
|
+
operation: str
|
|
25
|
+
code: str
|
|
26
|
+
session_id: bytes | None = None
|
|
27
|
+
run_id: bytes | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Observers:
|
|
31
|
+
"""One worker, at most 64 queued records/1 MiB, plus one running callback.
|
|
32
|
+
|
|
33
|
+
Records are copied before enqueue. Full queues drop the newest record.
|
|
34
|
+
close discards pending records and never waits for caller code. Python
|
|
35
|
+
cannot terminate a blocked callback: it may retain this one daemon thread.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, hooks: Any) -> None:
|
|
39
|
+
self._callbacks: dict[str, Callable[[Any], None]] = {}
|
|
40
|
+
if hooks is not None:
|
|
41
|
+
for name in hooks.__dataclass_fields__:
|
|
42
|
+
value = getattr(hooks, name)
|
|
43
|
+
if value is not None:
|
|
44
|
+
if (
|
|
45
|
+
not callable(value)
|
|
46
|
+
or iscoroutinefunction(value)
|
|
47
|
+
or iscoroutinefunction(type(value).__call__)
|
|
48
|
+
):
|
|
49
|
+
raise ValueError(f"{name} must be a synchronous callable")
|
|
50
|
+
self._callbacks[name] = value
|
|
51
|
+
self._condition = Condition()
|
|
52
|
+
self._queue: deque[tuple[str, Any, int]] = deque()
|
|
53
|
+
self._bytes = self._delivered = self._dropped = self._failed = 0
|
|
54
|
+
self._started = self._closed = False
|
|
55
|
+
|
|
56
|
+
def emit(self, name: str, value: Any) -> None:
|
|
57
|
+
if name not in self._callbacks:
|
|
58
|
+
return
|
|
59
|
+
# Protobuf records obey the wire limit. Other records are bounded SDK
|
|
60
|
+
# value objects. Do not serialize arbitrary application objects here.
|
|
61
|
+
size = _size(value)
|
|
62
|
+
with self._condition:
|
|
63
|
+
if self._closed or len(self._queue) >= 64 or self._bytes + size > 1048576:
|
|
64
|
+
self._dropped += 1
|
|
65
|
+
return
|
|
66
|
+
if not self._started:
|
|
67
|
+
try:
|
|
68
|
+
Thread(target=self._run, name="mppi-observers", daemon=True).start()
|
|
69
|
+
except RuntimeError:
|
|
70
|
+
self._failed += 1
|
|
71
|
+
return
|
|
72
|
+
self._started = True
|
|
73
|
+
self._queue.append((name, deepcopy(value), size))
|
|
74
|
+
self._bytes += size
|
|
75
|
+
self._condition.notify()
|
|
76
|
+
|
|
77
|
+
def _run(self) -> None:
|
|
78
|
+
while True:
|
|
79
|
+
with self._condition:
|
|
80
|
+
while not self._queue and not self._closed:
|
|
81
|
+
self._condition.wait()
|
|
82
|
+
if self._closed:
|
|
83
|
+
return
|
|
84
|
+
name, value, size = self._queue.popleft()
|
|
85
|
+
self._bytes -= size
|
|
86
|
+
try:
|
|
87
|
+
self._callbacks[name](value)
|
|
88
|
+
except BaseException:
|
|
89
|
+
# Caller callback failures, including SystemExit, cannot affect
|
|
90
|
+
# the SDK event loop, payment state or other queued callbacks.
|
|
91
|
+
with self._condition:
|
|
92
|
+
self._failed += 1
|
|
93
|
+
else:
|
|
94
|
+
with self._condition:
|
|
95
|
+
self._delivered += 1
|
|
96
|
+
|
|
97
|
+
def stats(self) -> ObserverStats:
|
|
98
|
+
with self._condition:
|
|
99
|
+
return ObserverStats(self._delivered, self._dropped, self._failed, len(self._queue))
|
|
100
|
+
|
|
101
|
+
def close(self) -> None:
|
|
102
|
+
with self._condition:
|
|
103
|
+
self._closed = True
|
|
104
|
+
self._dropped += len(self._queue)
|
|
105
|
+
self._queue.clear()
|
|
106
|
+
self._bytes = 0
|
|
107
|
+
self._condition.notify_all()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _size(value: Any) -> int:
|
|
111
|
+
if hasattr(value, "ByteSize"):
|
|
112
|
+
return int(value.ByteSize())
|
|
113
|
+
if isinstance(value, bytes):
|
|
114
|
+
return len(value)
|
|
115
|
+
if isinstance(value, str):
|
|
116
|
+
return len(value) * 4
|
|
117
|
+
if isinstance(value, tuple):
|
|
118
|
+
return sum(_size(item) for item in value)
|
|
119
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
120
|
+
return sum(_size(getattr(value, field.name)) for field in fields(value))
|
|
121
|
+
return 32
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Pure provider-report validation; callers supply and own their payment state."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
from .amounts import Rates, TokenCounts, numerator, priced_numerator
|
|
7
|
+
from .errors import ValidationCode, ValidationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RunStatus(StrEnum):
|
|
11
|
+
COMPLETED = "COMPLETED"
|
|
12
|
+
FAILED = "FAILED"
|
|
13
|
+
CANCELLED = "CANCELLED"
|
|
14
|
+
NOT_ADMITTED = "NOT_ADMITTED"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class RunAccounting:
|
|
19
|
+
counts: TokenCounts
|
|
20
|
+
remaining_numerator: int
|
|
21
|
+
final_status: RunStatus | None = None
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
if not isinstance(self.counts, TokenCounts):
|
|
25
|
+
raise ValidationError(ValidationCode.INVALID_VALUE, "counts must be TokenCounts")
|
|
26
|
+
numerator(self.remaining_numerator, field="remaining reservation")
|
|
27
|
+
if self.final_status is not None:
|
|
28
|
+
if (
|
|
29
|
+
not isinstance(self.final_status, RunStatus)
|
|
30
|
+
or self.final_status is RunStatus.NOT_ADMITTED
|
|
31
|
+
):
|
|
32
|
+
raise ValidationError(
|
|
33
|
+
ValidationCode.INVALID_VALUE, "admitted run requires a definitive final status"
|
|
34
|
+
)
|
|
35
|
+
if self.remaining_numerator != 0:
|
|
36
|
+
raise ValidationError(
|
|
37
|
+
ValidationCode.INVALID_VALUE, "final accounting has no remaining reservation"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class AccountingTransition:
|
|
43
|
+
state: RunAccounting
|
|
44
|
+
delta_numerator: int
|
|
45
|
+
released_numerator: int
|
|
46
|
+
duplicate: bool
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def report_transition(
|
|
50
|
+
previous: RunAccounting,
|
|
51
|
+
counts: TokenCounts,
|
|
52
|
+
rates: Rates,
|
|
53
|
+
*,
|
|
54
|
+
final_status: RunStatus | None = None,
|
|
55
|
+
) -> AccountingTransition:
|
|
56
|
+
"""Validate cumulative usage, conserve coverage and release only final slack.
|
|
57
|
+
|
|
58
|
+
This returns a calculation, not an admission grant, a commit operation,
|
|
59
|
+
proof of termination, or a new meter sequence. Runtime integration applies
|
|
60
|
+
it to a consistent authoritative session view supplied by the provider.
|
|
61
|
+
"""
|
|
62
|
+
if final_status is not None and (
|
|
63
|
+
not isinstance(final_status, RunStatus) or final_status is RunStatus.NOT_ADMITTED
|
|
64
|
+
):
|
|
65
|
+
raise ValidationError(ValidationCode.INVALID_VALUE, "invalid admitted-run final status")
|
|
66
|
+
old_numerator = priced_numerator(previous.counts, rates)
|
|
67
|
+
new_numerator = priced_numerator(counts, rates)
|
|
68
|
+
if previous.final_status is not None:
|
|
69
|
+
if counts == previous.counts and final_status == previous.final_status:
|
|
70
|
+
return AccountingTransition(previous, 0, 0, True)
|
|
71
|
+
raise ValidationError(
|
|
72
|
+
ValidationCode.INCONSISTENT_USAGE, "report conflicts with definitive final accounting"
|
|
73
|
+
)
|
|
74
|
+
old_counts = previous.counts
|
|
75
|
+
if (
|
|
76
|
+
counts.input_tokens < old_counts.input_tokens
|
|
77
|
+
or counts.cache_tokens < old_counts.cache_tokens
|
|
78
|
+
or counts.output_tokens < old_counts.output_tokens
|
|
79
|
+
or new_numerator < old_numerator
|
|
80
|
+
):
|
|
81
|
+
raise ValidationError(
|
|
82
|
+
ValidationCode.INCONSISTENT_USAGE,
|
|
83
|
+
"cumulative counts or exact recognized cost regressed",
|
|
84
|
+
)
|
|
85
|
+
delta = new_numerator - old_numerator
|
|
86
|
+
if delta > previous.remaining_numerator:
|
|
87
|
+
raise ValidationError(
|
|
88
|
+
ValidationCode.UNCOVERED_USAGE, "reported usage exceeds remaining funded reservation"
|
|
89
|
+
)
|
|
90
|
+
remaining = previous.remaining_numerator - delta
|
|
91
|
+
released = remaining if final_status is not None else 0
|
|
92
|
+
result = RunAccounting(counts, 0 if final_status is not None else remaining, final_status)
|
|
93
|
+
return AccountingTransition(
|
|
94
|
+
result, delta, released, counts == old_counts and final_status is None
|
|
95
|
+
)
|
mppi_protocol/amounts.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Exact MPPI amounts. All monetary outputs are six-decimal USDC base units."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .errors import ValidationCode, ValidationError
|
|
8
|
+
|
|
9
|
+
MAX_UINT64 = (1 << 64) - 1
|
|
10
|
+
RATE_SCALE = 1_000_000
|
|
11
|
+
_DECIMAL = re.compile(r"(?:0|[1-9][0-9]*)\Z", re.ASCII)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def uint64(value: int, *, field: str = "value", positive: bool = False) -> int:
|
|
15
|
+
"""Reject booleans, coercions and out-of-range integers before encoding."""
|
|
16
|
+
minimum = 1 if positive else 0
|
|
17
|
+
if type(value) is not int or not minimum <= value <= MAX_UINT64:
|
|
18
|
+
raise ValidationError(
|
|
19
|
+
ValidationCode.INVALID_VALUE, f"{field} must be a uint64 integer >= {minimum}"
|
|
20
|
+
)
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def decimal_uint64(value: str, *, field: str = "value") -> int:
|
|
25
|
+
"""Parse the exact catalog/TOML decimal-string grammar without float use."""
|
|
26
|
+
if type(value) is not str or len(value) > 20 or _DECIMAL.fullmatch(value) is None:
|
|
27
|
+
raise ValidationError(
|
|
28
|
+
ValidationCode.INVALID_VALUE, f"{field} must be a canonical decimal string"
|
|
29
|
+
)
|
|
30
|
+
return uint64(int(value), field=field)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def numerator(value: int, *, field: str = "numerator") -> int:
|
|
34
|
+
"""Validate a wide nonnegative integer; no uint64 intermediate truncation."""
|
|
35
|
+
if type(value) is not int or value < 0:
|
|
36
|
+
raise ValidationError(
|
|
37
|
+
ValidationCode.INVALID_VALUE, f"{field} must be a nonnegative integer"
|
|
38
|
+
)
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class Rates:
|
|
44
|
+
input: int
|
|
45
|
+
output: int
|
|
46
|
+
cache: int | None = None
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
uint64(self.input, field="input rate")
|
|
50
|
+
uint64(self.output, field="output rate")
|
|
51
|
+
if self.cache is not None:
|
|
52
|
+
uint64(self.cache, field="cache rate")
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def greatest(self) -> int:
|
|
56
|
+
return max(self.input, self.output, *(() if self.cache is None else (self.cache,)))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class TokenCounts:
|
|
61
|
+
"""Provider-supplied cumulative counts; cached input is a subset of input."""
|
|
62
|
+
|
|
63
|
+
input_tokens: int
|
|
64
|
+
cache_tokens: int
|
|
65
|
+
output_tokens: int
|
|
66
|
+
|
|
67
|
+
def __post_init__(self) -> None:
|
|
68
|
+
uint64(self.input_tokens, field="input_tokens")
|
|
69
|
+
uint64(self.cache_tokens, field="cache_tokens")
|
|
70
|
+
uint64(self.output_tokens, field="output_tokens")
|
|
71
|
+
if self.cache_tokens > self.input_tokens:
|
|
72
|
+
raise ValidationError(ValidationCode.INVALID_VALUE, "cache_tokens exceeds input_tokens")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def priced_numerator(counts: TokenCounts, rates: Rates) -> int:
|
|
76
|
+
"""Price supplied counts without rounding or adding a missing cache rate."""
|
|
77
|
+
if counts.cache_tokens and rates.cache is None:
|
|
78
|
+
raise ValidationError(
|
|
79
|
+
ValidationCode.INCONSISTENT_USAGE, "positive cache count has no accepted cache price"
|
|
80
|
+
)
|
|
81
|
+
return (
|
|
82
|
+
(counts.input_tokens - counts.cache_tokens) * rates.input
|
|
83
|
+
+ (counts.cache_tokens * rates.cache if rates.cache is not None else 0)
|
|
84
|
+
+ counts.output_tokens * rates.output
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cumulative_usage(numerators: Iterable[int]) -> int:
|
|
89
|
+
"""One floor after summing every accepted run/model/version numerator."""
|
|
90
|
+
return uint64(sum(numerator(n) for n in numerators) // RATE_SCALE, field="cumulative usage")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def required_coverage(numerators: Iterable[int]) -> int:
|
|
94
|
+
"""One ceiling over usage and all remaining/new bounds, never per row."""
|
|
95
|
+
total = sum(numerator(n) for n in numerators)
|
|
96
|
+
whole, remainder = divmod(total, RATE_SCALE)
|
|
97
|
+
return uint64(whole + bool(remainder), field="required coverage")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def funding_amount(tokens: int, rates: Rates) -> int:
|
|
101
|
+
"""Caller-chosen positive funding tokens at the greatest accepted rate."""
|
|
102
|
+
uint64(tokens, field="funding tokens", positive=True)
|
|
103
|
+
amount = required_coverage((tokens * rates.greatest,))
|
|
104
|
+
return uint64(amount, field="funding amount", positive=True)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def top_up_total(deposit: int, amount: int) -> int:
|
|
108
|
+
"""Validate positive raw-unit funding and cumulative deposit capacity."""
|
|
109
|
+
uint64(deposit, field="deposit", positive=True)
|
|
110
|
+
uint64(amount, field="top-up amount", positive=True)
|
|
111
|
+
return uint64(deposit + amount, field="cumulative deposit", positive=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def remaining_funding_tokens(
|
|
115
|
+
deposit: int, usage_numerator: int, reservation_numerators: Iterable[int], rates: Rates
|
|
116
|
+
) -> int | None:
|
|
117
|
+
"""Advisory deposit headroom; all-zero pricing has no token estimate."""
|
|
118
|
+
uint64(deposit, field="deposit")
|
|
119
|
+
numerator(usage_numerator)
|
|
120
|
+
reserved = sum(numerator(n) for n in reservation_numerators)
|
|
121
|
+
if rates.greatest == 0:
|
|
122
|
+
return None
|
|
123
|
+
available = max(0, deposit * RATE_SCALE - usage_numerator - reserved)
|
|
124
|
+
return min(MAX_UINT64, available // rates.greatest)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Wallet capability contracts. No wallet adapter is supplied by this module."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from .amounts import uint64
|
|
7
|
+
from .errors import ValidationCode, ValidationError
|
|
8
|
+
from .types import Domain, OperationResult, RunContext, fixed_bytes, uint256
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class WalletCall:
|
|
13
|
+
"""A nonpayable MPPI/registry call made by the configured wallet."""
|
|
14
|
+
|
|
15
|
+
chain_id: int
|
|
16
|
+
to: bytes
|
|
17
|
+
data: bytes
|
|
18
|
+
|
|
19
|
+
def __post_init__(self) -> None:
|
|
20
|
+
uint64(self.chain_id, field="chain_id", positive=True)
|
|
21
|
+
fixed_bytes(self.to, 20, field="to", nonzero=True)
|
|
22
|
+
if type(self.data) is not bytes:
|
|
23
|
+
raise ValidationError(ValidationCode.INVALID_VALUE, "calldata must be bytes")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class FundingAuthorization:
|
|
28
|
+
"""USDC receiveWithAuthorization input, in the token's separate domain."""
|
|
29
|
+
|
|
30
|
+
chain_id: int
|
|
31
|
+
token: bytes
|
|
32
|
+
domain_name: str
|
|
33
|
+
domain_version: str
|
|
34
|
+
owner: bytes
|
|
35
|
+
recipient: bytes
|
|
36
|
+
value: int
|
|
37
|
+
valid_after: int
|
|
38
|
+
valid_before: int
|
|
39
|
+
nonce: bytes
|
|
40
|
+
|
|
41
|
+
def __post_init__(self) -> None:
|
|
42
|
+
uint64(self.chain_id, field="chain_id", positive=True)
|
|
43
|
+
for name in ("token", "owner", "recipient"):
|
|
44
|
+
fixed_bytes(getattr(self, name), 20, field=name, nonzero=True)
|
|
45
|
+
for name in ("value", "valid_after", "valid_before"):
|
|
46
|
+
uint256(getattr(self, name), field=name)
|
|
47
|
+
fixed_bytes(self.nonce, 32, field="nonce")
|
|
48
|
+
if not self.domain_name or not self.domain_version or self.valid_after >= self.valid_before:
|
|
49
|
+
raise ValidationError(
|
|
50
|
+
ValidationCode.INVALID_VALUE, "invalid token domain or authorization interval"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TransactionWallet(Protocol):
|
|
55
|
+
@property
|
|
56
|
+
def address(self) -> bytes: ...
|
|
57
|
+
|
|
58
|
+
def execute(self, call: WalletCall, *, operation_ref: str) -> OperationResult: ...
|
|
59
|
+
|
|
60
|
+
def reconcile(self, *, operation_ref: str) -> OperationResult: ...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FundingWallet(TransactionWallet, Protocol):
|
|
64
|
+
def sign_funding_authorization(self, authorization: FundingAuthorization) -> bytes: ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SessionSigner(Protocol):
|
|
68
|
+
"""The fixed ECDSA capability, separate from wallet identity and authority."""
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def address(self) -> bytes: ...
|
|
72
|
+
|
|
73
|
+
def sign_voucher(self, domain: Domain, session_id: bytes, cumulative_amount: int) -> bytes: ...
|
|
74
|
+
|
|
75
|
+
def sign_run(self, domain: Domain, context: RunContext) -> bytes: ...
|
|
76
|
+
|
|
77
|
+
def sign_close(self, domain: Domain, session_id: bytes) -> bytes: ...
|
|
78
|
+
|
|
79
|
+
def sign_reconnect(self, domain: Domain, session_id: bytes, nonce: bytes) -> bytes: ...
|