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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swiftapi-python
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: SwiftAPI Python SDK - AI Action Verification Gateway
5
5
  Author-email: Rayan Pal <rayan@swiftapi.ai>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swiftapi-python"
7
- version = "1.0.2"
7
+ version = "1.1.0"
8
8
  description = "SwiftAPI Python SDK - AI Action Verification Gateway"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -29,13 +29,14 @@ Usage:
29
29
  db.update(user_id, data)
30
30
  """
31
31
 
32
- __version__ = "1.0.0"
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
- LOCK = "\U0001F512" # Locked
38
- UNLOCK = "\U0001F513" # Unlocked
39
- CHECK = "\u2713" # Checkmark
40
- CROSS = "\u2717" # X mark
41
- SHIELD = "\U0001F6E1" # Shield
42
- KEY = "\U0001F511" # Key
43
- WARNING = "\u26A0" # Warning
44
- LIGHTNING = "\u26A1" # Lightning bolt
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 signature fields (they weren't in the signed payload)
97
- signed_fields = {k: v for k, v in attestation.items() if k not in ("signature", "signing_mode")}
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=(",", ":"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swiftapi-python
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: SwiftAPI Python SDK - AI Action Verification Gateway
5
5
  Author-email: Rayan Pal <rayan@swiftapi.ai>
6
6
  License-Expression: MIT
@@ -4,6 +4,7 @@ swiftapi/__init__.py
4
4
  swiftapi/client.py
5
5
  swiftapi/enforcement.py
6
6
  swiftapi/exceptions.py
7
+ swiftapi/openai.py
7
8
  swiftapi/utils.py
8
9
  swiftapi/verifier.py
9
10
  swiftapi_python.egg-info/PKG-INFO