swiftapi-python 1.0.2__tar.gz → 1.1.0__tar.gz
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.
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/PKG-INFO +1 -1
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/pyproject.toml +1 -1
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi/__init__.py +4 -1
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi/client.py +24 -0
- swiftapi_python-1.1.0/swiftapi/openai.py +200 -0
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi/utils.py +20 -9
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi/verifier.py +5 -2
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi_python.egg-info/PKG-INFO +1 -1
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi_python.egg-info/SOURCES.txt +1 -0
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/README.md +0 -0
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/setup.cfg +0 -0
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi/enforcement.py +0 -0
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi/exceptions.py +0 -0
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi_python.egg-info/dependency_links.txt +0 -0
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi_python.egg-info/requires.txt +0 -0
- {swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi_python.egg-info/top_level.txt +0 -0
|
@@ -29,13 +29,14 @@ Usage:
|
|
|
29
29
|
db.update(user_id, data)
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
__version__ = "1.
|
|
32
|
+
__version__ = "1.1.0"
|
|
33
33
|
__author__ = "Rayan Pal"
|
|
34
34
|
|
|
35
35
|
# Core exports
|
|
36
36
|
from .client import SwiftAPI
|
|
37
37
|
from .enforcement import Enforcement, enforce
|
|
38
38
|
from .verifier import verify_signature, is_valid, get_public_key
|
|
39
|
+
from .openai import OpenAI
|
|
39
40
|
|
|
40
41
|
# Exceptions
|
|
41
42
|
from .exceptions import (
|
|
@@ -67,6 +68,8 @@ __all__ = [
|
|
|
67
68
|
"SwiftAPI",
|
|
68
69
|
"Enforcement",
|
|
69
70
|
"enforce",
|
|
71
|
+
# Drop-in OpenAI replacement
|
|
72
|
+
"OpenAI",
|
|
70
73
|
# Verification
|
|
71
74
|
"verify_signature",
|
|
72
75
|
"is_valid",
|
|
@@ -213,6 +213,30 @@ class SwiftAPI:
|
|
|
213
213
|
# If endpoint fails, assume not revoked (fail-open for availability)
|
|
214
214
|
return False
|
|
215
215
|
|
|
216
|
+
def attest(
|
|
217
|
+
self,
|
|
218
|
+
action_type: str,
|
|
219
|
+
action_data: Dict[str, Any],
|
|
220
|
+
) -> Dict[str, Any]:
|
|
221
|
+
"""
|
|
222
|
+
Request an execution attestation from the authority.
|
|
223
|
+
|
|
224
|
+
Calls /attest to get a signed attestation for the given action.
|
|
225
|
+
The attestation can then be passed to gated endpoints like /chat/vibe.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
action_type: Type of action (e.g., "chat_completion")
|
|
229
|
+
action_data: Action payload to be attested
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Signed attestation dict with jti, signature, action_fingerprint, etc.
|
|
233
|
+
"""
|
|
234
|
+
payload = {
|
|
235
|
+
"action_type": action_type,
|
|
236
|
+
"action_data": action_data,
|
|
237
|
+
}
|
|
238
|
+
return self._request("POST", "/attest", json=payload)
|
|
239
|
+
|
|
216
240
|
# =========================================================================
|
|
217
241
|
# Info Endpoints
|
|
218
242
|
# =========================================================================
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SwiftAPI Attested OpenAI - Drop-in replacement.
|
|
3
|
+
|
|
4
|
+
# Before (vibe coding):
|
|
5
|
+
from openai import OpenAI
|
|
6
|
+
client = OpenAI()
|
|
7
|
+
|
|
8
|
+
# After (attested):
|
|
9
|
+
from swiftapi import OpenAI
|
|
10
|
+
client = OpenAI(swiftapi_key="swiftapi_live_...")
|
|
11
|
+
|
|
12
|
+
Same interface. Same return type. Every completion is cryptographically attested.
|
|
13
|
+
No attestation, no output.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import requests
|
|
18
|
+
from typing import List, Dict, Union, Optional, Any
|
|
19
|
+
|
|
20
|
+
from .exceptions import SwiftAPIError, AuthenticationError, NetworkError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Message:
|
|
24
|
+
"""OpenAI-compatible message object."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, role: str, content: str):
|
|
27
|
+
self.role = role
|
|
28
|
+
self.content = content
|
|
29
|
+
|
|
30
|
+
def __repr__(self):
|
|
31
|
+
truncated = self.content[:80] if self.content else ""
|
|
32
|
+
return f"Message(role='{self.role}', content='{truncated}')"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Choice:
|
|
36
|
+
"""OpenAI-compatible choice object."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, index: int, message: Message, finish_reason: str):
|
|
39
|
+
self.index = index
|
|
40
|
+
self.message = message
|
|
41
|
+
self.finish_reason = finish_reason
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ChatCompletion:
|
|
45
|
+
"""OpenAI-compatible chat completion response."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, content: str, model: str, attested: bool = False, jti: str = None):
|
|
48
|
+
self.choices = [
|
|
49
|
+
Choice(
|
|
50
|
+
index=0,
|
|
51
|
+
message=Message(role="assistant", content=content),
|
|
52
|
+
finish_reason="stop" if content else "void",
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
self.model = model
|
|
56
|
+
self.attested = attested
|
|
57
|
+
self.jti = jti
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def content(self):
|
|
61
|
+
"""Convenience: response.content instead of response.choices[0].message.content"""
|
|
62
|
+
return self.choices[0].message.content
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Completions:
|
|
66
|
+
"""Handles chat.completions.create() calls."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, key: str, base_url: str, timeout: int):
|
|
69
|
+
self._key = key
|
|
70
|
+
self._base_url = base_url.rstrip("/")
|
|
71
|
+
self._timeout = timeout
|
|
72
|
+
self._session = requests.Session()
|
|
73
|
+
self._session.headers.update({
|
|
74
|
+
"X-SwiftAPI-Authority": key,
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"User-Agent": "swiftapi-python/1.1.0",
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
def create(
|
|
80
|
+
self,
|
|
81
|
+
model: str,
|
|
82
|
+
messages: List[Dict[str, Any]],
|
|
83
|
+
temperature: float = 0.7,
|
|
84
|
+
max_completion_tokens: int = 300,
|
|
85
|
+
max_tokens: Optional[int] = None,
|
|
86
|
+
**kwargs,
|
|
87
|
+
) -> ChatCompletion:
|
|
88
|
+
"""
|
|
89
|
+
Create an attested chat completion.
|
|
90
|
+
|
|
91
|
+
Same interface as openai.chat.completions.create().
|
|
92
|
+
Attestation is handled transparently. If any gate fails, content is empty string.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
model: Model name (e.g. "gpt-4o-mini")
|
|
96
|
+
messages: List of message dicts with role and content
|
|
97
|
+
temperature: Sampling temperature
|
|
98
|
+
max_completion_tokens: Max tokens in response
|
|
99
|
+
max_tokens: Alias for max_completion_tokens (OpenAI compat)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
ChatCompletion with .choices[0].message.content
|
|
103
|
+
"""
|
|
104
|
+
payload = {
|
|
105
|
+
"model": model,
|
|
106
|
+
"messages": messages,
|
|
107
|
+
"temperature": temperature,
|
|
108
|
+
"max_completion_tokens": max_tokens or max_completion_tokens,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Step 1: Attest
|
|
112
|
+
try:
|
|
113
|
+
attest_resp = self._session.post(
|
|
114
|
+
f"{self._base_url}/attest",
|
|
115
|
+
json={"action_type": "chat_completion", "action_data": payload},
|
|
116
|
+
timeout=self._timeout,
|
|
117
|
+
)
|
|
118
|
+
except requests.exceptions.RequestException as e:
|
|
119
|
+
raise NetworkError(f"Attestation request failed: {e}")
|
|
120
|
+
|
|
121
|
+
if attest_resp.status_code == 403:
|
|
122
|
+
raise AuthenticationError("Invalid SwiftAPI authority key")
|
|
123
|
+
if attest_resp.status_code == 429:
|
|
124
|
+
raise SwiftAPIError("Rate limit exceeded")
|
|
125
|
+
if attest_resp.status_code != 200:
|
|
126
|
+
raise SwiftAPIError(f"Attestation failed: {attest_resp.text[:200]}")
|
|
127
|
+
|
|
128
|
+
attestation = attest_resp.json()
|
|
129
|
+
|
|
130
|
+
if not attestation.get("approved"):
|
|
131
|
+
return ChatCompletion(content="", model=model)
|
|
132
|
+
|
|
133
|
+
# Step 2: Forward to /chat/vibe with attestation
|
|
134
|
+
try:
|
|
135
|
+
vibe_resp = self._session.post(
|
|
136
|
+
f"{self._base_url}/chat/vibe",
|
|
137
|
+
json=payload,
|
|
138
|
+
headers={"X-SwiftAPI-Attestation": json.dumps(attestation)},
|
|
139
|
+
timeout=self._timeout,
|
|
140
|
+
)
|
|
141
|
+
except requests.exceptions.RequestException as e:
|
|
142
|
+
raise NetworkError(f"Chat request failed: {e}")
|
|
143
|
+
|
|
144
|
+
if vibe_resp.status_code == 403:
|
|
145
|
+
raise AuthenticationError("Invalid SwiftAPI authority key")
|
|
146
|
+
if vibe_resp.status_code != 200:
|
|
147
|
+
raise SwiftAPIError(f"Chat failed: {vibe_resp.text[:200]}")
|
|
148
|
+
|
|
149
|
+
content = vibe_resp.json()
|
|
150
|
+
if content is None:
|
|
151
|
+
content = ""
|
|
152
|
+
|
|
153
|
+
return ChatCompletion(
|
|
154
|
+
content=content,
|
|
155
|
+
model=model,
|
|
156
|
+
attested=True,
|
|
157
|
+
jti=attestation.get("jti"),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class _ChatNamespace:
|
|
162
|
+
"""Namespace for client.chat.completions"""
|
|
163
|
+
|
|
164
|
+
def __init__(self, key: str, base_url: str, timeout: int):
|
|
165
|
+
self.completions = Completions(key, base_url, timeout)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class OpenAI:
|
|
169
|
+
"""
|
|
170
|
+
Drop-in replacement for openai.OpenAI.
|
|
171
|
+
|
|
172
|
+
Every completion is cryptographically attested via SwiftAPI.
|
|
173
|
+
No attestation, no output.
|
|
174
|
+
|
|
175
|
+
Usage:
|
|
176
|
+
from swiftapi import OpenAI
|
|
177
|
+
|
|
178
|
+
client = OpenAI(swiftapi_key="swiftapi_live_...")
|
|
179
|
+
r = client.chat.completions.create(
|
|
180
|
+
model="gpt-4o-mini",
|
|
181
|
+
messages=[{"role": "user", "content": "Hello"}]
|
|
182
|
+
)
|
|
183
|
+
print(r.choices[0].message.content)
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
swiftapi_key: str = None,
|
|
189
|
+
api_key: str = None,
|
|
190
|
+
base_url: str = "https://swiftapi.ai",
|
|
191
|
+
timeout: int = 60,
|
|
192
|
+
**kwargs,
|
|
193
|
+
):
|
|
194
|
+
key = swiftapi_key or api_key
|
|
195
|
+
if not key:
|
|
196
|
+
raise AuthenticationError("swiftapi_key is required")
|
|
197
|
+
if not key.startswith("swiftapi_live_"):
|
|
198
|
+
raise AuthenticationError("Invalid key format: must start with 'swiftapi_live_'")
|
|
199
|
+
|
|
200
|
+
self.chat = _ChatNamespace(key, base_url, timeout)
|
|
@@ -30,18 +30,29 @@ class Colors:
|
|
|
30
30
|
RED = GREEN = YELLOW = BLUE = MAGENTA = CYAN = WHITE = RESET = BOLD = ""
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
# Unicode symbols
|
|
33
|
+
# Unicode symbols - Windows-safe fallbacks
|
|
34
34
|
class Symbols:
|
|
35
35
|
"""Unicode symbols for SwiftAPI output."""
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
if sys.platform == 'win32':
|
|
38
|
+
# Windows console can't encode emoji - use ASCII fallbacks
|
|
39
|
+
LOCK = "[DENIED]"
|
|
40
|
+
UNLOCK = "[UNLOCKED]"
|
|
41
|
+
CHECK = "[OK]"
|
|
42
|
+
CROSS = "[X]"
|
|
43
|
+
SHIELD = "[VERIFIED]"
|
|
44
|
+
KEY = "[KEY]"
|
|
45
|
+
WARNING = "[!]"
|
|
46
|
+
LIGHTNING = "[*]"
|
|
47
|
+
else:
|
|
48
|
+
LOCK = "\U0001F512" # Locked
|
|
49
|
+
UNLOCK = "\U0001F513" # Unlocked
|
|
50
|
+
CHECK = "\u2713" # Checkmark
|
|
51
|
+
CROSS = "\u2717" # X mark
|
|
52
|
+
SHIELD = "\U0001F6E1" # Shield
|
|
53
|
+
KEY = "\U0001F511" # Key
|
|
54
|
+
WARNING = "\u26A0" # Warning
|
|
55
|
+
LIGHTNING = "\u26A1" # Lightning bolt
|
|
45
56
|
|
|
46
57
|
|
|
47
58
|
def print_approved(action_type: str, intent: str):
|
|
@@ -93,8 +93,11 @@ def _reconstruct_signed_payload(attestation: Dict[str, Any]) -> bytes:
|
|
|
93
93
|
The server signs the attestation BEFORE adding signature/signing_mode fields.
|
|
94
94
|
We must reconstruct that exact payload.
|
|
95
95
|
"""
|
|
96
|
-
# Copy attestation and remove
|
|
97
|
-
|
|
96
|
+
# Copy attestation and remove fields that weren't in the signed payload.
|
|
97
|
+
# signature/signing_mode are added after signing.
|
|
98
|
+
# action_data/denial_reason are added by /attest after signing.
|
|
99
|
+
_unsigned = ("signature", "signing_mode", "action_data", "denial_reason")
|
|
100
|
+
signed_fields = {k: v for k, v in attestation.items() if k not in _unsigned}
|
|
98
101
|
|
|
99
102
|
# Deterministic JSON serialization (must match server exactly)
|
|
100
103
|
payload_str = json.dumps(signed_fields, sort_keys=True, separators=(",", ":"))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{swiftapi_python-1.0.2 → swiftapi_python-1.1.0}/swiftapi_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|