vaultedge 1.0.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.
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaultedge
3
+ Version: 1.0.0
4
+ Summary: VaultEdge — zero-trust AI key manager SDK for Python
5
+ License: MIT
6
+ Project-URL: Homepage, https://vaultedge.dev
7
+ Project-URL: Repository, https://github.com/you/vaultedge
8
+ Keywords: ai,llm,api-key,vault,openai,security,proxy
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security :: Cryptography
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: cryptography>=42.0.0
22
+ Requires-Dist: httpx>=0.27.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
25
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vaultedge"
7
+ version = "1.0.0"
8
+ description = "VaultEdge — zero-trust AI key manager SDK for Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ keywords = ["ai", "llm", "api-key", "vault", "openai", "security", "proxy"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Security :: Cryptography",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "cryptography>=42.0.0",
27
+ "httpx>=0.27.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://vaultedge.dev"
35
+ Repository = "https://github.com/you/vaultedge"
36
+
37
+ [tool.pytest.ini_options]
38
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ """VaultEdge — zero-trust AI key manager SDK for Python."""
2
+
3
+ from .vault import VaultEntry, encrypt_vault, decrypt_vault, VAULT_PREFIX
4
+ from .client import VaultEdge, resolve_provider
5
+
6
+ __all__ = [
7
+ "VaultEdge",
8
+ "VaultEntry",
9
+ "encrypt_vault",
10
+ "decrypt_vault",
11
+ "resolve_provider",
12
+ "VAULT_PREFIX",
13
+ ]
14
+
15
+ __version__ = "1.0.0"
@@ -0,0 +1,271 @@
1
+ """
2
+ VaultEdge Python SDK — main client.
3
+
4
+ Usage:
5
+ import asyncio
6
+ import os
7
+ from vaultedge import VaultEdge
8
+
9
+ ve = VaultEdge(
10
+ vault=os.environ["VAULTEDGE_VAULT"],
11
+ password=os.environ["VAULTEDGE_PASSWORD"],
12
+ )
13
+
14
+ async def main():
15
+ response = await ve.chat.completions.create(
16
+ model="gpt-4o",
17
+ messages=[{"role": "user", "content": "Hello!"}],
18
+ )
19
+ print(response["choices"][0]["message"]["content"])
20
+
21
+ asyncio.run(main())
22
+ """
23
+
24
+ import os
25
+ from typing import Any, AsyncIterator, Dict, List, Optional, Union
26
+ import httpx
27
+
28
+ from .vault import VaultEntry, decrypt_vault
29
+
30
+ # ─── Provider Config ───────────────────────────────────────────────────────────
31
+
32
+ PROVIDER_CONFIGS: Dict[str, Dict[str, Any]] = {
33
+ "OpenAI": {"url": "https://api.openai.com/v1/chat/completions", "scheme": "bearer"},
34
+ "Groq": {"url": "https://api.groq.com/openai/v1/chat/completions", "scheme": "bearer"},
35
+ "Anthropic": {"url": "https://api.anthropic.com/v1/messages", "scheme": "x-api-key"},
36
+ "Gemini": {"url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", "scheme": "bearer"},
37
+ "Mistral": {"url": "https://api.mistral.ai/v1/chat/completions", "scheme": "bearer"},
38
+ "xAI": {"url": "https://api.x.ai/v1/chat/completions", "scheme": "bearer"},
39
+ "DeepSeek": {"url": "https://api.deepseek.com/v1/chat/completions", "scheme": "bearer"},
40
+ "OpenRouter": {"url": "https://openrouter.ai/api/v1/chat/completions", "scheme": "bearer"},
41
+ "Cohere": {"url": "https://api.cohere.ai/v1/chat/completions", "scheme": "bearer"},
42
+ "Cerebras": {"url": "https://api.cerebras.ai/v1/chat/completions", "scheme": "bearer"},
43
+ "Sambanova": {"url": "https://api.sambanova.ai/v1/chat/completions", "scheme": "bearer"},
44
+ "Nvidia": {"url": "https://integrate.api.nvidia.com/v1/chat/completions", "scheme": "bearer"},
45
+ "Together": {"url": "https://api.together.xyz/v1/chat/completions", "scheme": "bearer"},
46
+ "Perplexity": {"url": "https://api.perplexity.ai/chat/completions", "scheme": "bearer"},
47
+ }
48
+
49
+ MODEL_PREFIX_MAP = [
50
+ ("gpt-", "OpenAI"),
51
+ ("o1", "OpenAI"),
52
+ ("o3", "OpenAI"),
53
+ ("davinci", "OpenAI"),
54
+ ("llama", "Groq"),
55
+ ("groq", "Groq"),
56
+ ("mixtral", "Groq"),
57
+ ("gemma", "Groq"),
58
+ ("gemini", "Gemini"),
59
+ ("claude", "Anthropic"),
60
+ ("mistral", "Mistral"),
61
+ ("codestral", "Mistral"),
62
+ ("grok", "xAI"),
63
+ ("deepseek", "DeepSeek"),
64
+ ("command", "Cohere"),
65
+ ("cohere", "Cohere"),
66
+ ("nvidia", "Nvidia"),
67
+ ("nim", "Nvidia"),
68
+ ("sonar", "Perplexity"),
69
+ ("pplx-", "Perplexity"),
70
+ ]
71
+
72
+
73
+ def resolve_provider(model: str) -> Optional[str]:
74
+ lower = model.lower()
75
+ for prefix, provider in MODEL_PREFIX_MAP:
76
+ if lower.startswith(prefix):
77
+ return provider
78
+ return None
79
+
80
+
81
+ def build_headers(provider: str, api_key: str) -> Dict[str, str]:
82
+ config = PROVIDER_CONFIGS.get(provider, {})
83
+ scheme = config.get("scheme", "bearer")
84
+ headers = {"Content-Type": "application/json"}
85
+ if scheme == "bearer":
86
+ headers["Authorization"] = f"Bearer {api_key}"
87
+ elif scheme == "x-api-key":
88
+ headers["x-api-key"] = api_key
89
+ headers["anthropic-version"] = "2023-06-01"
90
+ return headers
91
+
92
+
93
+ # ─── Completions ───────────────────────────────────────────────────────────────
94
+
95
+ class Completions:
96
+ def __init__(self, client: "VaultEdge") -> None:
97
+ self._client = client
98
+
99
+ async def create(
100
+ self,
101
+ model: str,
102
+ messages: List[Dict[str, Any]],
103
+ *,
104
+ stream: bool = False,
105
+ temperature: Optional[float] = None,
106
+ max_tokens: Optional[int] = None,
107
+ top_p: Optional[float] = None,
108
+ **kwargs: Any,
109
+ ) -> Union[Dict[str, Any], AsyncIterator[Dict[str, Any]]]:
110
+ """
111
+ Create a chat completion.
112
+
113
+ Args:
114
+ model: Model name (e.g. "gpt-4o", "claude-3-5-sonnet-latest")
115
+ messages: List of message dicts with "role" and "content"
116
+ stream: If True, returns an async iterator of chunk dicts
117
+ temperature: Sampling temperature
118
+ max_tokens: Max tokens to generate
119
+ **kwargs: Any other OpenAI-compatible parameters
120
+
121
+ Returns:
122
+ If stream=False: A dict matching the OpenAI ChatCompletion format.
123
+ If stream=True: An async iterator of chunk dicts.
124
+ """
125
+ entries = await self._client._get_entries()
126
+ provider_keys: Dict[str, List[str]] = {}
127
+ for e in entries:
128
+ provider_keys.setdefault(e.provider, []).append(e.key)
129
+
130
+ primary = resolve_provider(model)
131
+ if not primary:
132
+ raise ValueError(f"No provider found for model '{model}'.")
133
+
134
+ order = [primary] + [p for p in PROVIDER_CONFIGS if p != primary]
135
+ payload: Dict[str, Any] = {
136
+ "model": model,
137
+ "messages": messages,
138
+ "stream": stream,
139
+ **kwargs,
140
+ }
141
+ if temperature is not None:
142
+ payload["temperature"] = temperature
143
+ if max_tokens is not None:
144
+ payload["max_tokens"] = max_tokens
145
+ if top_p is not None:
146
+ payload["top_p"] = top_p
147
+
148
+ errors = []
149
+ attempts = 0
150
+
151
+ for provider in order:
152
+ keys = provider_keys.get(provider, [])
153
+ if not keys:
154
+ continue
155
+ if attempts >= self._client.max_retries:
156
+ break
157
+
158
+ config = PROVIDER_CONFIGS.get(provider)
159
+ if not config:
160
+ continue
161
+
162
+ for key in keys:
163
+ if attempts >= self._client.max_retries:
164
+ break
165
+ attempts += 1
166
+ headers = build_headers(provider, key)
167
+ try:
168
+ async with httpx.AsyncClient(timeout=self._client.timeout) as http:
169
+ if stream:
170
+ return self._stream_response(http, config["url"], headers, payload)
171
+ resp = await http.post(config["url"], json=payload, headers=headers)
172
+ if resp.status_code == 200:
173
+ return resp.json()
174
+ errors.append(f"{provider}: HTTP {resp.status_code}")
175
+ except Exception as exc:
176
+ errors.append(f"{provider}: {exc}")
177
+
178
+ raise RuntimeError(f"All providers failed: {'; '.join(errors)}")
179
+
180
+ async def _stream_response(
181
+ self,
182
+ http: httpx.AsyncClient,
183
+ url: str,
184
+ headers: Dict[str, str],
185
+ payload: Dict[str, Any],
186
+ ) -> AsyncIterator[Dict[str, Any]]:
187
+ import json as _json
188
+ async with http.stream("POST", url, json=payload, headers=headers) as resp:
189
+ async for line in resp.aiter_lines():
190
+ if line.startswith("data: "):
191
+ data = line[6:]
192
+ if data == "[DONE]":
193
+ break
194
+ try:
195
+ yield _json.loads(data)
196
+ except _json.JSONDecodeError:
197
+ pass
198
+
199
+
200
+ class Chat:
201
+ def __init__(self, client: "VaultEdge") -> None:
202
+ self.completions = Completions(client)
203
+
204
+
205
+ # ─── Main Client ───────────────────────────────────────────────────────────────
206
+
207
+ class VaultEdge:
208
+ """
209
+ VaultEdge Python SDK client.
210
+
211
+ Example::
212
+
213
+ ve = VaultEdge(
214
+ vault=os.environ["VAULTEDGE_VAULT"],
215
+ password=os.environ["VAULTEDGE_PASSWORD"],
216
+ )
217
+ response = await ve.chat.completions.create(
218
+ model="gpt-4o",
219
+ messages=[{"role": "user", "content": "Hello!"}],
220
+ )
221
+ """
222
+
223
+ def __init__(
224
+ self,
225
+ vault: Optional[str] = None,
226
+ password: Optional[str] = None,
227
+ *,
228
+ timeout: float = 60.0,
229
+ max_retries: int = 3,
230
+ debug: bool = False,
231
+ ) -> None:
232
+ self._vault_string = vault or os.environ.get("VAULTEDGE_VAULT")
233
+ self._password = password or os.environ.get("VAULTEDGE_PASSWORD")
234
+
235
+ if not self._vault_string:
236
+ raise ValueError(
237
+ "No vault provided. Pass vault= or set VAULTEDGE_VAULT env var."
238
+ )
239
+ if not self._password:
240
+ raise ValueError(
241
+ "No password provided. Pass password= or set VAULTEDGE_PASSWORD env var."
242
+ )
243
+
244
+ self.timeout = timeout
245
+ self.max_retries = max_retries
246
+ self.debug = debug
247
+ self.chat = Chat(self)
248
+ self._cached_entries: Optional[List[VaultEntry]] = None
249
+
250
+ async def _get_entries(self) -> List[VaultEntry]:
251
+ if self._cached_entries is None:
252
+ if self.debug:
253
+ print("[vaultedge] Decrypting vault...")
254
+ self._cached_entries = decrypt_vault(self._vault_string, self._password) # type: ignore[arg-type]
255
+ if self.debug:
256
+ print(f"[vaultedge] Vault decrypted: {len(self._cached_entries)} entries")
257
+ return self._cached_entries
258
+
259
+ async def get_providers(self) -> List[str]:
260
+ """Return list of providers available in the vault."""
261
+ entries = await self._get_entries()
262
+ return list({e.provider for e in entries})
263
+
264
+ async def has_provider(self, provider: str) -> bool:
265
+ """Check if the vault has a key for the given provider."""
266
+ entries = await self._get_entries()
267
+ return any(e.provider == provider for e in entries)
268
+
269
+ def resolve_model(self, model: str) -> Optional[str]:
270
+ """Resolve which provider will handle a given model name."""
271
+ return resolve_provider(model)
@@ -0,0 +1,107 @@
1
+ """
2
+ VaultEdge vault crypto — AES-256-GCM + PBKDF2.
3
+
4
+ Wire format:
5
+ Prefix: "VE_VAULT_v1_"
6
+ Payload (base64): salt[32] + nonce[12] + AES-256-GCM ciphertext+tag
7
+ """
8
+
9
+ import base64
10
+ import json
11
+ import os
12
+ from dataclasses import dataclass
13
+ from typing import List
14
+
15
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
16
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
17
+ from cryptography.hazmat.primitives import hashes
18
+
19
+ VAULT_PREFIX = "VE_VAULT_v1_"
20
+ PBKDF2_ITERATIONS = 210_000
21
+ SALT_BYTES = 32
22
+ NONCE_BYTES = 12
23
+
24
+
25
+ @dataclass
26
+ class VaultEntry:
27
+ """A decrypted API key entry from the vault."""
28
+ provider: str
29
+ key: str
30
+
31
+
32
+ def _derive_key(password: str, salt: bytes) -> bytes:
33
+ """Derive a 256-bit AES key from a password using PBKDF2-HMAC-SHA256."""
34
+ kdf = PBKDF2HMAC(
35
+ algorithm=hashes.SHA256(),
36
+ length=32,
37
+ salt=salt,
38
+ iterations=PBKDF2_ITERATIONS,
39
+ )
40
+ return kdf.derive(password.encode("utf-8"))
41
+
42
+
43
+ def encrypt_vault(entries: List[VaultEntry], password: str) -> str:
44
+ """
45
+ Encrypt a list of VaultEntry objects with a master password.
46
+
47
+ Returns a string starting with VE_VAULT_v1_ suitable for an env var.
48
+ """
49
+ salt = os.urandom(SALT_BYTES)
50
+ nonce = os.urandom(NONCE_BYTES)
51
+ key = _derive_key(password, salt)
52
+
53
+ plaintext = json.dumps(
54
+ [{"provider": e.provider, "key": e.key} for e in entries]
55
+ ).encode("utf-8")
56
+
57
+ aesgcm = AESGCM(key)
58
+ ciphertext = aesgcm.encrypt(nonce, plaintext, None)
59
+
60
+ wire = salt + nonce + ciphertext
61
+ b64 = base64.b64encode(wire).decode("ascii")
62
+ return f"{VAULT_PREFIX}{b64}"
63
+
64
+
65
+ def decrypt_vault(vault_string: str, password: str) -> List[VaultEntry]:
66
+ """
67
+ Decrypt a VaultEdge vault string.
68
+
69
+ Raises:
70
+ ValueError: If the vault string is invalid or the password is wrong.
71
+ """
72
+ if vault_string.startswith(VAULT_PREFIX):
73
+ b64 = vault_string[len(VAULT_PREFIX):]
74
+ else:
75
+ raise ValueError(
76
+ f"Invalid vault format. Expected string starting with '{VAULT_PREFIX}'."
77
+ )
78
+
79
+ try:
80
+ wire = base64.b64decode(b64)
81
+ except Exception as exc:
82
+ raise ValueError("Vault string is not valid base64.") from exc
83
+
84
+ if len(wire) < SALT_BYTES + NONCE_BYTES + 16:
85
+ raise ValueError("Vault data is too short to be valid.")
86
+
87
+ salt = wire[:SALT_BYTES]
88
+ nonce = wire[SALT_BYTES:SALT_BYTES + NONCE_BYTES]
89
+ ciphertext = wire[SALT_BYTES + NONCE_BYTES:]
90
+
91
+ key = _derive_key(password, salt)
92
+ aesgcm = AESGCM(key)
93
+
94
+ try:
95
+ plaintext = aesgcm.decrypt(nonce, ciphertext, None)
96
+ except Exception as exc:
97
+ raise ValueError(
98
+ "Decryption failed. The password is incorrect or the vault is corrupted."
99
+ ) from exc
100
+
101
+ try:
102
+ data = json.loads(plaintext.decode("utf-8"))
103
+ if not isinstance(data, list):
104
+ raise ValueError("Vault decrypted but contained invalid JSON structure.")
105
+ return [VaultEntry(provider=item["provider"], key=item["key"]) for item in data]
106
+ except (json.JSONDecodeError, KeyError) as exc:
107
+ raise ValueError("Vault decrypted but contained invalid JSON.") from exc
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaultedge
3
+ Version: 1.0.0
4
+ Summary: VaultEdge — zero-trust AI key manager SDK for Python
5
+ License: MIT
6
+ Project-URL: Homepage, https://vaultedge.dev
7
+ Project-URL: Repository, https://github.com/you/vaultedge
8
+ Keywords: ai,llm,api-key,vault,openai,security,proxy
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security :: Cryptography
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: cryptography>=42.0.0
22
+ Requires-Dist: httpx>=0.27.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
25
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
@@ -0,0 +1,9 @@
1
+ pyproject.toml
2
+ vaultedge/__init__.py
3
+ vaultedge/client.py
4
+ vaultedge/vault.py
5
+ vaultedge.egg-info/PKG-INFO
6
+ vaultedge.egg-info/SOURCES.txt
7
+ vaultedge.egg-info/dependency_links.txt
8
+ vaultedge.egg-info/requires.txt
9
+ vaultedge.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ cryptography>=42.0.0
2
+ httpx>=0.27.0
3
+
4
+ [dev]
5
+ pytest>=8.0.0
6
+ pytest-asyncio>=0.23.0
@@ -0,0 +1 @@
1
+ vaultedge