akumi 0.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.
@@ -0,0 +1,39 @@
1
+ name: Publish to PyPI
2
+
3
+ # Fires on the vX.Y.Z tag that the control-plane sdk-python.yml workflow
4
+ # auto-pushes. Sets pyproject.toml to the tag version, builds, and uploads to
5
+ # PyPI via Trusted Publishing (OIDC, no stored token). Skips cleanly until the
6
+ # PYPI_PUBLISH repo variable is set to 'true', so early tags do not fail.
7
+ on:
8
+ push:
9
+ tags: ['v*']
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ id-token: write
16
+ steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Setup Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: '3.12'
24
+
25
+ - name: Set version from the tag
26
+ run: sed -i -E "s/^version = \".*\"/version = \"${GITHUB_REF_NAME#v}\"/" pyproject.toml
27
+
28
+ - name: Build wheel and sdist
29
+ run: |
30
+ python -m pip install --upgrade build
31
+ python -m build
32
+
33
+ - name: Publish to PyPI (Trusted Publishing)
34
+ if: vars.PYPI_PUBLISH == 'true'
35
+ uses: pypa/gh-action-pypi-publish@release/v1
36
+
37
+ - name: Publishing skipped
38
+ if: vars.PYPI_PUBLISH != 'true'
39
+ run: echo "PyPI publishing not enabled; set repo variable PYPI_PUBLISH=true after configuring Trusted Publishing."
akumi-0.1.0/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ build/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ *.egg-info/
akumi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: akumi
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Akumi EU-sovereign inference API.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: httpx>=0.27
8
+ Provides-Extra: dev
9
+ Requires-Dist: mypy>=1.11; extra == 'dev'
10
+ Requires-Dist: ruff>=0.6; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Akumi Python SDK
14
+
15
+ The official Python client for [Akumi](https://akumi.cloud), the EU-sovereign,
16
+ OpenAI-compatible inference API. Built on httpx, with both a synchronous and an
17
+ asynchronous client. One `base_url` for every model, governed and metered, with
18
+ your regulated data kept in the EU.
19
+
20
+ - **Drop-in OpenAI-compatible.** Chat completions, embeddings, and models under one key.
21
+ - **EU-sovereign by default.** The egress guard fails closed on non-EU routing.
22
+ - **Governed, not just hosted.** PII firewall, per-request residency, and a metadata-only audit trail on every call.
23
+ - **Sync and async.** Streaming and automatic retries in both clients.
24
+
25
+ ## Requirements
26
+
27
+ Python 3.9 or newer.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install akumi
33
+ ```
34
+
35
+ ## Quickstart
36
+
37
+ Create an API key under app.akumi.cloud -> Platform -> API keys:
38
+
39
+ ```python
40
+ from akumi import Akumi
41
+
42
+ client = Akumi.from_api_key("mk_...")
43
+
44
+ result = client.chat.create(params={
45
+ "model": "mistral/mistral-large-latest",
46
+ "messages": [
47
+ {"role": "user", "content": "Explain EU data residency in one sentence."},
48
+ ],
49
+ })
50
+
51
+ print(result["choices"][0]["message"]["content"])
52
+ ```
53
+
54
+ ## Streaming
55
+
56
+ ```python
57
+ for chunk in client.chat.create_streamed(params={
58
+ "model": "mistral/mistral-large-latest",
59
+ "messages": [{"role": "user", "content": "Write a haiku about Frankfurt."}],
60
+ }):
61
+ print(chunk["choices"][0]["delta"].get("content", ""), end="")
62
+ ```
63
+
64
+ ## Async
65
+
66
+ ```python
67
+ import asyncio
68
+ from akumi import AsyncAkumi
69
+
70
+ async def main():
71
+ client = AsyncAkumi.from_api_key("mk_...")
72
+ result = await client.chat.create(params={
73
+ "model": "mistral/mistral-large-latest",
74
+ "messages": [{"role": "user", "content": "Hello from the EU."}],
75
+ })
76
+ print(result["choices"][0]["message"]["content"])
77
+
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ ## Embeddings
82
+
83
+ ```python
84
+ embeddings = client.embeddings.create(params={
85
+ "model": "mistral/mistral-embed",
86
+ "input": "The quarterly report is ready for review.",
87
+ })
88
+
89
+ vector = embeddings["data"][0]["embedding"]
90
+ ```
91
+
92
+ ## More resources
93
+
94
+ - `client.models.list()` lists the models available to your key.
95
+ - `client.memory.forget(...)` and `client.memoryThreads.list()` manage long-term memory and threads.
96
+ - `client.auditLogs.list()` reads your metadata-only audit trail.
97
+
98
+ ## Configuration
99
+
100
+ `from_api_key()` connects to `https://api.akumi.cloud/v1` and retries transient
101
+ failures (429, 502, 503, 504). Pass `base_url=` to target another host.
102
+
103
+ ## Documentation
104
+
105
+ - Guides: https://akumi.cloud/docs
106
+ - API reference: https://akumi.cloud/docs/api-reference
107
+
108
+ ## About
109
+
110
+ Generated from the Akumi OpenAPI specification, so it tracks the API
111
+ automatically. Issues: https://github.com/akumi-cloud/python-sdk.
112
+
113
+ MIT licensed.
akumi-0.1.0/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Akumi Python SDK
2
+
3
+ The official Python client for [Akumi](https://akumi.cloud), the EU-sovereign,
4
+ OpenAI-compatible inference API. Built on httpx, with both a synchronous and an
5
+ asynchronous client. One `base_url` for every model, governed and metered, with
6
+ your regulated data kept in the EU.
7
+
8
+ - **Drop-in OpenAI-compatible.** Chat completions, embeddings, and models under one key.
9
+ - **EU-sovereign by default.** The egress guard fails closed on non-EU routing.
10
+ - **Governed, not just hosted.** PII firewall, per-request residency, and a metadata-only audit trail on every call.
11
+ - **Sync and async.** Streaming and automatic retries in both clients.
12
+
13
+ ## Requirements
14
+
15
+ Python 3.9 or newer.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install akumi
21
+ ```
22
+
23
+ ## Quickstart
24
+
25
+ Create an API key under app.akumi.cloud -> Platform -> API keys:
26
+
27
+ ```python
28
+ from akumi import Akumi
29
+
30
+ client = Akumi.from_api_key("mk_...")
31
+
32
+ result = client.chat.create(params={
33
+ "model": "mistral/mistral-large-latest",
34
+ "messages": [
35
+ {"role": "user", "content": "Explain EU data residency in one sentence."},
36
+ ],
37
+ })
38
+
39
+ print(result["choices"][0]["message"]["content"])
40
+ ```
41
+
42
+ ## Streaming
43
+
44
+ ```python
45
+ for chunk in client.chat.create_streamed(params={
46
+ "model": "mistral/mistral-large-latest",
47
+ "messages": [{"role": "user", "content": "Write a haiku about Frankfurt."}],
48
+ }):
49
+ print(chunk["choices"][0]["delta"].get("content", ""), end="")
50
+ ```
51
+
52
+ ## Async
53
+
54
+ ```python
55
+ import asyncio
56
+ from akumi import AsyncAkumi
57
+
58
+ async def main():
59
+ client = AsyncAkumi.from_api_key("mk_...")
60
+ result = await client.chat.create(params={
61
+ "model": "mistral/mistral-large-latest",
62
+ "messages": [{"role": "user", "content": "Hello from the EU."}],
63
+ })
64
+ print(result["choices"][0]["message"]["content"])
65
+
66
+ asyncio.run(main())
67
+ ```
68
+
69
+ ## Embeddings
70
+
71
+ ```python
72
+ embeddings = client.embeddings.create(params={
73
+ "model": "mistral/mistral-embed",
74
+ "input": "The quarterly report is ready for review.",
75
+ })
76
+
77
+ vector = embeddings["data"][0]["embedding"]
78
+ ```
79
+
80
+ ## More resources
81
+
82
+ - `client.models.list()` lists the models available to your key.
83
+ - `client.memory.forget(...)` and `client.memoryThreads.list()` manage long-term memory and threads.
84
+ - `client.auditLogs.list()` reads your metadata-only audit trail.
85
+
86
+ ## Configuration
87
+
88
+ `from_api_key()` connects to `https://api.akumi.cloud/v1` and retries transient
89
+ failures (429, 502, 503, 504). Pass `base_url=` to target another host.
90
+
91
+ ## Documentation
92
+
93
+ - Guides: https://akumi.cloud/docs
94
+ - API reference: https://akumi.cloud/docs/api-reference
95
+
96
+ ## About
97
+
98
+ Generated from the Akumi OpenAPI specification, so it tracks the API
99
+ automatically. Issues: https://github.com/akumi-cloud/python-sdk.
100
+
101
+ MIT licensed.
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "akumi"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Akumi EU-sovereign inference API."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ dependencies = ["httpx>=0.27"]
13
+
14
+ [project.optional-dependencies]
15
+ dev = ["mypy>=1.11", "ruff>=0.6"]
16
+
17
+ [tool.hatch.build.targets.wheel]
18
+ packages = ["src/akumi"]
19
+
20
+ [tool.ruff]
21
+ target-version = "py39"
22
+
23
+ [tool.ruff.lint]
24
+ # The transport's __enter__/__aenter__ return the concrete transport type rather
25
+ # than Self, which on Python < 3.11 would pull in typing_extensions just for an
26
+ # annotation. PYI034 is a stylistic preference, so ignore it and keep the runtime
27
+ # dependency-free. All other lint rules are auto-fixed in CI (ruff check --fix).
28
+ ignore = ["PYI034"]
29
+
30
+ [tool.mypy]
31
+ python_version = "3.9"
32
+ ignore_missing_imports = true
@@ -0,0 +1,48 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from ._async_client import AsyncAkumi
5
+ from ._client import Akumi
6
+ from ._config import ClientConfig
7
+ from ._errors import (
8
+ AkumiError,
9
+ ApiError,
10
+ AuthenticationError,
11
+ InvalidRequestError,
12
+ RateLimitError,
13
+ )
14
+ from .models import (
15
+ AuditLogApiResource,
16
+ ChatCompletionsRequest,
17
+ EmbeddingsRequest,
18
+ IngestDocumentRequest,
19
+ RememberFactRequest,
20
+ SearchDocumentsRequest,
21
+ SearchFactsRequest,
22
+ SearchRequest,
23
+ StoreCollectionRequest,
24
+ ThreadMessageViewModel,
25
+ ThreadViewModel,
26
+ )
27
+
28
+ __all__ = [
29
+ "Akumi",
30
+ "AkumiError",
31
+ "ApiError",
32
+ "AsyncAkumi",
33
+ "AuditLogApiResource",
34
+ "AuthenticationError",
35
+ "ChatCompletionsRequest",
36
+ "ClientConfig",
37
+ "EmbeddingsRequest",
38
+ "IngestDocumentRequest",
39
+ "InvalidRequestError",
40
+ "RateLimitError",
41
+ "RememberFactRequest",
42
+ "SearchDocumentsRequest",
43
+ "SearchFactsRequest",
44
+ "SearchRequest",
45
+ "StoreCollectionRequest",
46
+ "ThreadMessageViewModel",
47
+ "ThreadViewModel",
48
+ ]
@@ -0,0 +1,33 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from ._config import ClientConfig
5
+ from ._transport import AsyncTransport
6
+ from .resources.auditLogs_async import AsyncAuditLogsResource
7
+ from .resources.chat_async import AsyncChatResource
8
+ from .resources.embeddings_async import AsyncEmbeddingsResource
9
+ from .resources.models_async import AsyncModelsResource
10
+ from .resources.recall_async import AsyncRecallResource
11
+
12
+
13
+ class AsyncAkumi:
14
+ def __init__(
15
+ self,
16
+ *,
17
+ api_key: str,
18
+ base_url: str = "https://api.akumi.cloud/v1",
19
+ max_retries: int = 2,
20
+ ) -> None:
21
+ config = ClientConfig(
22
+ api_key=api_key, base_url=base_url, max_retries=max_retries
23
+ )
24
+ self._transport = AsyncTransport(config)
25
+ self.recall = AsyncRecallResource(self._transport)
26
+ self.auditLogs = AsyncAuditLogsResource(self._transport)
27
+ self.chat = AsyncChatResource(self._transport)
28
+ self.embeddings = AsyncEmbeddingsResource(self._transport)
29
+ self.models = AsyncModelsResource(self._transport)
30
+
31
+ @classmethod
32
+ def from_api_key(cls, api_key: str) -> AsyncAkumi:
33
+ return cls(api_key=api_key)
@@ -0,0 +1,33 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from ._config import ClientConfig
5
+ from ._transport import SyncTransport
6
+ from .resources.auditLogs_sync import AuditLogsResource
7
+ from .resources.chat_sync import ChatResource
8
+ from .resources.embeddings_sync import EmbeddingsResource
9
+ from .resources.models_sync import ModelsResource
10
+ from .resources.recall_sync import RecallResource
11
+
12
+
13
+ class Akumi:
14
+ def __init__(
15
+ self,
16
+ *,
17
+ api_key: str,
18
+ base_url: str = "https://api.akumi.cloud/v1",
19
+ max_retries: int = 2,
20
+ ) -> None:
21
+ config = ClientConfig(
22
+ api_key=api_key, base_url=base_url, max_retries=max_retries
23
+ )
24
+ self._transport = SyncTransport(config)
25
+ self.recall = RecallResource(self._transport)
26
+ self.auditLogs = AuditLogsResource(self._transport)
27
+ self.chat = ChatResource(self._transport)
28
+ self.embeddings = EmbeddingsResource(self._transport)
29
+ self.models = ModelsResource(self._transport)
30
+
31
+ @classmethod
32
+ def from_api_key(cls, api_key: str) -> Akumi:
33
+ return cls(api_key=api_key)
@@ -0,0 +1,16 @@
1
+ # AUTO-GENERATED runtime, copied by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class ClientConfig:
9
+ """Immutable client configuration. The api_key is sent only on the
10
+ Authorization header by the transport and is never logged or placed in a
11
+ query string."""
12
+
13
+ api_key: str
14
+ base_url: str = "https://api.akumi.cloud/v1"
15
+ max_retries: int = 2
16
+ retry_on: tuple[int, ...] = field(default_factory=lambda: (429, 500, 502, 503, 504))
@@ -0,0 +1,52 @@
1
+ # AUTO-GENERATED runtime, copied by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+
7
+ class AkumiError(Exception):
8
+ """Base class for every error raised by the SDK."""
9
+
10
+
11
+ class ApiError(AkumiError):
12
+ """An error returned by the API. Carries the HTTP status and decoded body."""
13
+
14
+ def __init__(
15
+ self, message: str, status: int, body: dict[str, Any] | None = None
16
+ ) -> None:
17
+ super().__init__(message)
18
+ self.status = status
19
+ self.body: dict[str, Any] = body if body is not None else {}
20
+
21
+
22
+ class AuthenticationError(ApiError):
23
+ """Raised on a 401 or 403 response."""
24
+
25
+
26
+ class RateLimitError(ApiError):
27
+ """Raised on a 429 response."""
28
+
29
+
30
+ class InvalidRequestError(ApiError):
31
+ """Raised on a 4xx response that is not 401, 403, or 429."""
32
+
33
+
34
+ def _message_for(status: int, body: dict[str, Any]) -> str:
35
+ error = body.get("error")
36
+ if isinstance(error, dict):
37
+ message = error.get("message")
38
+ if isinstance(message, str):
39
+ return message
40
+ return f"HTTP {status}"
41
+
42
+
43
+ def map_error(status: int, body: dict[str, Any]) -> ApiError:
44
+ """Map an HTTP status and decoded body onto the matching typed exception."""
45
+ message = _message_for(status, body)
46
+ if status in (401, 403):
47
+ return AuthenticationError(message, status, body)
48
+ if status == 429:
49
+ return RateLimitError(message, status, body)
50
+ if 400 <= status < 500:
51
+ return InvalidRequestError(message, status, body)
52
+ return ApiError(message, status, body)
@@ -0,0 +1,21 @@
1
+ # AUTO-GENERATED runtime, copied by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+ DONE_SENTINEL = "[DONE]"
8
+
9
+
10
+ def parse_sse_line(line: str) -> dict[str, Any] | None:
11
+ """Decode a single SSE line. Returns the parsed event object, or None when
12
+ the line is not a data line, is blank, or is the [DONE] sentinel."""
13
+ if not line.startswith("data:"):
14
+ return None
15
+
16
+ data = line[len("data:") :].strip()
17
+ if data == "" or data == DONE_SENTINEL:
18
+ return None
19
+
20
+ decoded = json.loads(data)
21
+ return decoded if isinstance(decoded, dict) else None
@@ -0,0 +1,219 @@
1
+ # AUTO-GENERATED runtime, copied by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import time
7
+ from collections.abc import AsyncGenerator, Generator
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from ._config import ClientConfig
13
+ from ._errors import map_error
14
+ from ._sse import parse_sse_line
15
+
16
+
17
+ def _build_url(base_url: str, path: str, query: dict[str, Any] | None) -> str:
18
+ url = base_url.rstrip("/") + path
19
+ if query:
20
+ pairs = {key: value for key, value in query.items() if value is not None}
21
+ if pairs:
22
+ url = url + "?" + str(httpx.QueryParams(pairs))
23
+ return url
24
+
25
+
26
+ def _decode(text: str) -> dict[str, Any]:
27
+ if text == "":
28
+ return {}
29
+ decoded = json.loads(text)
30
+ return decoded if isinstance(decoded, dict) else {}
31
+
32
+
33
+ def _error_body(text: str) -> dict[str, Any]:
34
+ if text == "":
35
+ return {}
36
+ try:
37
+ decoded = json.loads(text)
38
+ except json.JSONDecodeError:
39
+ return {}
40
+ return decoded if isinstance(decoded, dict) else {}
41
+
42
+
43
+ class SyncTransport:
44
+ """Synchronous HTTP transport: bearer auth, JSON, retries with backoff,
45
+ status-to-exception mapping, and incremental SSE reads. The api_key is sent
46
+ only on the Authorization header and is never logged."""
47
+
48
+ def __init__(self, config: ClientConfig) -> None:
49
+ self._config = config
50
+ self._client = httpx.Client()
51
+
52
+ def close(self) -> None:
53
+ self._client.close()
54
+
55
+ def __enter__(self) -> SyncTransport:
56
+ return self
57
+
58
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
59
+ self.close()
60
+
61
+ def request(
62
+ self,
63
+ method: str,
64
+ path: str,
65
+ query: dict[str, Any] | None,
66
+ body: dict[str, Any] | None,
67
+ ) -> dict[str, Any]:
68
+ response = self._dispatch(method, path, query, body)
69
+ return _decode(response.text)
70
+
71
+ def stream(
72
+ self,
73
+ method: str,
74
+ path: str,
75
+ body: dict[str, Any] | None,
76
+ ) -> Generator[dict[str, Any], None, None]:
77
+ url = _build_url(self._config.base_url, path, None)
78
+ headers = {
79
+ "Authorization": f"Bearer {self._config.api_key}",
80
+ "Accept": "text/event-stream",
81
+ }
82
+ content = json.dumps(body) if body is not None else None
83
+ if content is not None:
84
+ headers["Content-Type"] = "application/json"
85
+ with self._client.stream(
86
+ method, url, headers=headers, content=content
87
+ ) as response:
88
+ if response.status_code >= 400:
89
+ response.read()
90
+ raise map_error(response.status_code, _error_body(response.text))
91
+ for line in response.iter_lines():
92
+ event = parse_sse_line(line)
93
+ if event is not None:
94
+ yield event
95
+
96
+ def _dispatch(
97
+ self,
98
+ method: str,
99
+ path: str,
100
+ query: dict[str, Any] | None,
101
+ body: dict[str, Any] | None,
102
+ ) -> httpx.Response:
103
+ url = _build_url(self._config.base_url, path, query)
104
+ headers = {
105
+ "Authorization": f"Bearer {self._config.api_key}",
106
+ "Accept": "application/json",
107
+ }
108
+ content = json.dumps(body) if body is not None else None
109
+ if content is not None:
110
+ headers["Content-Type"] = "application/json"
111
+
112
+ attempt = 0
113
+ while True:
114
+ response = self._client.request(
115
+ method, url, headers=headers, content=content
116
+ )
117
+ if response.status_code < 400:
118
+ return response
119
+
120
+ should_retry = (
121
+ attempt < self._config.max_retries
122
+ and response.status_code in self._config.retry_on
123
+ )
124
+ if should_retry:
125
+ attempt += 1
126
+ time.sleep(0.25 * (2 ** (attempt - 1)))
127
+ continue
128
+
129
+ raise map_error(response.status_code, _error_body(response.text))
130
+
131
+
132
+ class AsyncTransport:
133
+ """Asynchronous HTTP transport. Same behavior as SyncTransport over
134
+ httpx.AsyncClient. The api_key is sent only on the Authorization header."""
135
+
136
+ def __init__(self, config: ClientConfig) -> None:
137
+ self._config = config
138
+ self._client = httpx.AsyncClient()
139
+
140
+ async def aclose(self) -> None:
141
+ await self._client.aclose()
142
+
143
+ async def __aenter__(self) -> AsyncTransport:
144
+ return self
145
+
146
+ async def __aexit__(
147
+ self, exc_type: object, exc_val: object, exc_tb: object
148
+ ) -> None:
149
+ await self.aclose()
150
+
151
+ async def arequest(
152
+ self,
153
+ method: str,
154
+ path: str,
155
+ query: dict[str, Any] | None,
156
+ body: dict[str, Any] | None,
157
+ ) -> dict[str, Any]:
158
+ response = await self._adispatch(method, path, query, body)
159
+ return _decode(response.text)
160
+
161
+ async def astream(
162
+ self,
163
+ method: str,
164
+ path: str,
165
+ body: dict[str, Any] | None,
166
+ ) -> AsyncGenerator[dict[str, Any], None]:
167
+ url = _build_url(self._config.base_url, path, None)
168
+ headers = {
169
+ "Authorization": f"Bearer {self._config.api_key}",
170
+ "Accept": "text/event-stream",
171
+ }
172
+ content = json.dumps(body) if body is not None else None
173
+ if content is not None:
174
+ headers["Content-Type"] = "application/json"
175
+ async with self._client.stream(
176
+ method, url, headers=headers, content=content
177
+ ) as response:
178
+ if response.status_code >= 400:
179
+ await response.aread()
180
+ raise map_error(response.status_code, _error_body(response.text))
181
+ async for line in response.aiter_lines():
182
+ event = parse_sse_line(line)
183
+ if event is not None:
184
+ yield event
185
+
186
+ async def _adispatch(
187
+ self,
188
+ method: str,
189
+ path: str,
190
+ query: dict[str, Any] | None,
191
+ body: dict[str, Any] | None,
192
+ ) -> httpx.Response:
193
+ url = _build_url(self._config.base_url, path, query)
194
+ headers = {
195
+ "Authorization": f"Bearer {self._config.api_key}",
196
+ "Accept": "application/json",
197
+ }
198
+ content = json.dumps(body) if body is not None else None
199
+ if content is not None:
200
+ headers["Content-Type"] = "application/json"
201
+
202
+ attempt = 0
203
+ while True:
204
+ response = await self._client.request(
205
+ method, url, headers=headers, content=content
206
+ )
207
+ if response.status_code < 400:
208
+ return response
209
+
210
+ should_retry = (
211
+ attempt < self._config.max_retries
212
+ and response.status_code in self._config.retry_on
213
+ )
214
+ if should_retry:
215
+ attempt += 1
216
+ await asyncio.sleep(0.25 * (2 ** (attempt - 1)))
217
+ continue
218
+
219
+ raise map_error(response.status_code, _error_body(response.text))
@@ -0,0 +1,210 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+
8
+ @dataclass
9
+ class AuditLogApiResource:
10
+ id: str
11
+ component: str
12
+ action: str
13
+ actorId: int | None
14
+ ipAddress: str | None
15
+ userAgent: str | None
16
+ target: Any | None
17
+ metadata: list[Any] | None
18
+ createdAt: str
19
+
20
+ @classmethod
21
+ def from_dict(cls, data: dict[str, Any]) -> AuditLogApiResource:
22
+ return cls(
23
+ id=data["id"],
24
+ component=data["component"],
25
+ action=data["action"],
26
+ actorId=data["actorId"],
27
+ ipAddress=data["ipAddress"],
28
+ userAgent=data["userAgent"],
29
+ target=data["target"],
30
+ metadata=data["metadata"],
31
+ createdAt=data["createdAt"],
32
+ )
33
+
34
+
35
+ @dataclass
36
+ class ChatCompletionsRequest:
37
+ model: str
38
+ messages: list[Any]
39
+ temperature: float | None = None
40
+ max_tokens: int | None = None
41
+ stream: bool | None = None
42
+ firewall: bool | None = None
43
+ rag: str | None = None
44
+ user: str | None = None
45
+ thread: str | None = None
46
+ top_p: float | None = None
47
+ presence_penalty: float | None = None
48
+ frequency_penalty: float | None = None
49
+ n: int | None = None
50
+ seed: int | None = None
51
+ logprobs: bool | None = None
52
+ top_logprobs: int | None = None
53
+ max_completion_tokens: int | None = None
54
+ tool_choice: str | None = None
55
+ parallel_tool_calls: bool | None = None
56
+ cache: bool | None = None
57
+ stop: list[str] | None = None
58
+ logit_bias: list[float] | None = None
59
+ response_format: Any | None = None
60
+ tools: list[Any] | None = None
61
+
62
+ @classmethod
63
+ def from_dict(cls, data: dict[str, Any]) -> ChatCompletionsRequest:
64
+ return cls(
65
+ model=data["model"],
66
+ messages=data["messages"],
67
+ temperature=data.get("temperature"),
68
+ max_tokens=data.get("max_tokens"),
69
+ stream=data.get("stream"),
70
+ firewall=data.get("firewall"),
71
+ rag=data.get("rag"),
72
+ user=data.get("user"),
73
+ thread=data.get("thread"),
74
+ top_p=data.get("top_p"),
75
+ presence_penalty=data.get("presence_penalty"),
76
+ frequency_penalty=data.get("frequency_penalty"),
77
+ n=data.get("n"),
78
+ seed=data.get("seed"),
79
+ logprobs=data.get("logprobs"),
80
+ top_logprobs=data.get("top_logprobs"),
81
+ max_completion_tokens=data.get("max_completion_tokens"),
82
+ tool_choice=data.get("tool_choice"),
83
+ parallel_tool_calls=data.get("parallel_tool_calls"),
84
+ cache=data.get("cache"),
85
+ stop=data.get("stop"),
86
+ logit_bias=data.get("logit_bias"),
87
+ response_format=data.get("response_format"),
88
+ tools=data.get("tools"),
89
+ )
90
+
91
+
92
+ @dataclass
93
+ class EmbeddingsRequest:
94
+ model: str
95
+ input: list[str]
96
+ encoding_format: str | None = None
97
+ dimensions: int | None = None
98
+ user: str | None = None
99
+
100
+ @classmethod
101
+ def from_dict(cls, data: dict[str, Any]) -> EmbeddingsRequest:
102
+ return cls(
103
+ model=data["model"],
104
+ input=data["input"],
105
+ encoding_format=data.get("encoding_format"),
106
+ dimensions=data.get("dimensions"),
107
+ user=data.get("user"),
108
+ )
109
+
110
+
111
+ @dataclass
112
+ class IngestDocumentRequest:
113
+ title: str
114
+ text: str
115
+ source: str | None = None
116
+ collection: str | None = None
117
+
118
+ @classmethod
119
+ def from_dict(cls, data: dict[str, Any]) -> IngestDocumentRequest:
120
+ return cls(
121
+ title=data["title"],
122
+ text=data["text"],
123
+ source=data.get("source"),
124
+ collection=data.get("collection"),
125
+ )
126
+
127
+
128
+ @dataclass
129
+ class RememberFactRequest:
130
+ content: str
131
+ user_ref: str
132
+
133
+ @classmethod
134
+ def from_dict(cls, data: dict[str, Any]) -> RememberFactRequest:
135
+ return cls(
136
+ content=data["content"],
137
+ user_ref=data["user_ref"],
138
+ )
139
+
140
+
141
+ @dataclass
142
+ class SearchDocumentsRequest:
143
+ query: str
144
+ collection: str
145
+ user_ref: str | None = None
146
+ limit: int | None = None
147
+
148
+ @classmethod
149
+ def from_dict(cls, data: dict[str, Any]) -> SearchDocumentsRequest:
150
+ return cls(
151
+ query=data["query"],
152
+ collection=data["collection"],
153
+ user_ref=data.get("user_ref"),
154
+ limit=data.get("limit"),
155
+ )
156
+
157
+
158
+ @dataclass
159
+ class SearchFactsRequest:
160
+ query: str
161
+ user_ref: str | None = None
162
+ limit: int | None = None
163
+
164
+ @classmethod
165
+ def from_dict(cls, data: dict[str, Any]) -> SearchFactsRequest:
166
+ return cls(
167
+ query=data["query"],
168
+ user_ref=data.get("user_ref"),
169
+ limit=data.get("limit"),
170
+ )
171
+
172
+
173
+ @dataclass
174
+ class SearchRequest:
175
+ query: str
176
+ user_ref: str | None = None
177
+ limit: int | None = None
178
+
179
+ @classmethod
180
+ def from_dict(cls, data: dict[str, Any]) -> SearchRequest:
181
+ return cls(
182
+ query=data["query"],
183
+ user_ref=data.get("user_ref"),
184
+ limit=data.get("limit"),
185
+ )
186
+
187
+
188
+ @dataclass
189
+ class StoreCollectionRequest:
190
+ name: str
191
+
192
+ @classmethod
193
+ def from_dict(cls, data: dict[str, Any]) -> StoreCollectionRequest:
194
+ return cls(
195
+ name=data["name"],
196
+ )
197
+
198
+
199
+ @dataclass
200
+ class ThreadMessageViewModel:
201
+ @classmethod
202
+ def from_dict(cls, data: dict[str, Any]) -> ThreadMessageViewModel:
203
+ return cls()
204
+
205
+
206
+ @dataclass
207
+ class ThreadViewModel:
208
+ @classmethod
209
+ def from_dict(cls, data: dict[str, Any]) -> ThreadViewModel:
210
+ return cls()
@@ -0,0 +1,17 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .._transport import AsyncTransport
7
+
8
+
9
+ class AsyncAuditLogsResource:
10
+ def __init__(self, transport: AsyncTransport) -> None:
11
+ self._transport = transport
12
+
13
+ async def list(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
14
+ return await self._transport.arequest("GET", "/audit-logs", query, None)
15
+
16
+ async def get(self, uuid: str) -> dict[str, Any]:
17
+ return await self._transport.arequest("GET", f"/audit-logs/{uuid}", None, None)
@@ -0,0 +1,17 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .._transport import SyncTransport
7
+
8
+
9
+ class AuditLogsResource:
10
+ def __init__(self, transport: SyncTransport) -> None:
11
+ self._transport = transport
12
+
13
+ def list(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
14
+ return self._transport.request("GET", "/audit-logs", query, None)
15
+
16
+ def get(self, uuid: str) -> dict[str, Any]:
17
+ return self._transport.request("GET", f"/audit-logs/{uuid}", None, None)
@@ -0,0 +1,21 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import AsyncIterator
5
+ from typing import Any
6
+
7
+ from .._transport import AsyncTransport
8
+
9
+
10
+ class AsyncChatResource:
11
+ def __init__(self, transport: AsyncTransport) -> None:
12
+ self._transport = transport
13
+
14
+ async def create(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
15
+ return await self._transport.arequest("POST", "/chat/completions", None, params)
16
+
17
+ async def create_streamed(
18
+ self, params: dict[str, Any] | None = None
19
+ ) -> AsyncIterator[dict[str, Any]]:
20
+ async for event in self._transport.astream("POST", "/chat/completions", params):
21
+ yield event
@@ -0,0 +1,20 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Iterator
5
+ from typing import Any
6
+
7
+ from .._transport import SyncTransport
8
+
9
+
10
+ class ChatResource:
11
+ def __init__(self, transport: SyncTransport) -> None:
12
+ self._transport = transport
13
+
14
+ def create(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
15
+ return self._transport.request("POST", "/chat/completions", None, params)
16
+
17
+ def create_streamed(
18
+ self, params: dict[str, Any] | None = None
19
+ ) -> Iterator[dict[str, Any]]:
20
+ yield from self._transport.stream("POST", "/chat/completions", params)
@@ -0,0 +1,14 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .._transport import AsyncTransport
7
+
8
+
9
+ class AsyncEmbeddingsResource:
10
+ def __init__(self, transport: AsyncTransport) -> None:
11
+ self._transport = transport
12
+
13
+ async def create(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
14
+ return await self._transport.arequest("POST", "/embeddings", None, params)
@@ -0,0 +1,14 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .._transport import SyncTransport
7
+
8
+
9
+ class EmbeddingsResource:
10
+ def __init__(self, transport: SyncTransport) -> None:
11
+ self._transport = transport
12
+
13
+ def create(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
14
+ return self._transport.request("POST", "/embeddings", None, params)
@@ -0,0 +1,14 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .._transport import AsyncTransport
7
+
8
+
9
+ class AsyncModelsResource:
10
+ def __init__(self, transport: AsyncTransport) -> None:
11
+ self._transport = transport
12
+
13
+ async def list(self) -> dict[str, Any]:
14
+ return await self._transport.arequest("GET", "/models", None, None)
@@ -0,0 +1,14 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .._transport import SyncTransport
7
+
8
+
9
+ class ModelsResource:
10
+ def __init__(self, transport: SyncTransport) -> None:
11
+ self._transport = transport
12
+
13
+ def list(self) -> dict[str, Any]:
14
+ return self._transport.request("GET", "/models", None, None)
@@ -0,0 +1,110 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .._transport import AsyncTransport
7
+
8
+
9
+ class AsyncRecallResource:
10
+ def __init__(self, transport: AsyncTransport) -> None:
11
+ self._transport = transport
12
+
13
+ async def listThreads(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
14
+ return await self._transport.arequest("GET", "/recall/threads", query, None)
15
+
16
+ async def createThread(self) -> dict[str, Any]:
17
+ return await self._transport.arequest("POST", "/recall/threads", None, None)
18
+
19
+ async def getThread(self, thread: str) -> dict[str, Any]:
20
+ return await self._transport.arequest(
21
+ "GET", f"/recall/threads/{thread}", None, None
22
+ )
23
+
24
+ async def deleteThread(self, thread: str) -> dict[str, Any]:
25
+ return await self._transport.arequest(
26
+ "DELETE", f"/recall/threads/{thread}", None, None
27
+ )
28
+
29
+ async def search(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
30
+ return await self._transport.arequest("POST", "/recall/search", None, params)
31
+
32
+ async def searchFacts(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
33
+ return await self._transport.arequest(
34
+ "POST", "/recall/facts/search", None, params
35
+ )
36
+
37
+ async def listFacts(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
38
+ return await self._transport.arequest("GET", "/recall/facts", query, None)
39
+
40
+ async def rememberFact(
41
+ self, params: dict[str, Any] | None = None
42
+ ) -> dict[str, Any]:
43
+ return await self._transport.arequest("POST", "/recall/facts", None, params)
44
+
45
+ async def forgetFact(self, id: str) -> dict[str, Any]:
46
+ return await self._transport.arequest(
47
+ "DELETE", f"/recall/facts/{id}", None, None
48
+ )
49
+
50
+ async def export(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
51
+ return await self._transport.arequest("GET", "/recall/export", query, None)
52
+
53
+ async def erase(self) -> dict[str, Any]:
54
+ return await self._transport.arequest("DELETE", "/recall", None, None)
55
+
56
+ async def searchDocuments(
57
+ self, params: dict[str, Any] | None = None
58
+ ) -> dict[str, Any]:
59
+ return await self._transport.arequest(
60
+ "POST", "/recall/documents/search", None, params
61
+ )
62
+
63
+ async def listDocuments(
64
+ self, query: dict[str, Any] | None = None
65
+ ) -> dict[str, Any]:
66
+ return await self._transport.arequest("GET", "/recall/documents", query, None)
67
+
68
+ async def ingestDocument(
69
+ self, params: dict[str, Any] | None = None
70
+ ) -> dict[str, Any]:
71
+ return await self._transport.arequest("POST", "/recall/documents", None, params)
72
+
73
+ async def getDocument(self, document: str) -> dict[str, Any]:
74
+ return await self._transport.arequest(
75
+ "GET", f"/recall/documents/{document}", None, None
76
+ )
77
+
78
+ async def deleteDocument(self, document: str) -> dict[str, Any]:
79
+ return await self._transport.arequest(
80
+ "DELETE", f"/recall/documents/{document}", None, None
81
+ )
82
+
83
+ async def listCollections(
84
+ self, query: dict[str, Any] | None = None
85
+ ) -> dict[str, Any]:
86
+ return await self._transport.arequest("GET", "/recall/collections", query, None)
87
+
88
+ async def createCollection(
89
+ self, params: dict[str, Any] | None = None
90
+ ) -> dict[str, Any]:
91
+ return await self._transport.arequest(
92
+ "POST", "/recall/collections", None, params
93
+ )
94
+
95
+ async def getCollection(self, slug: str) -> dict[str, Any]:
96
+ return await self._transport.arequest(
97
+ "GET", f"/recall/collections/{slug}", None, None
98
+ )
99
+
100
+ async def deleteCollection(self, slug: str) -> dict[str, Any]:
101
+ return await self._transport.arequest(
102
+ "DELETE", f"/recall/collections/{slug}", None, None
103
+ )
104
+
105
+ async def updateCollection(
106
+ self, slug: str, params: dict[str, Any] | None = None
107
+ ) -> dict[str, Any]:
108
+ return await self._transport.arequest(
109
+ "PATCH", f"/recall/collections/{slug}", None, params
110
+ )
@@ -0,0 +1,86 @@
1
+ # AUTO-GENERATED by akumi/codegen. Do not edit by hand.
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .._transport import SyncTransport
7
+
8
+
9
+ class RecallResource:
10
+ def __init__(self, transport: SyncTransport) -> None:
11
+ self._transport = transport
12
+
13
+ def listThreads(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
14
+ return self._transport.request("GET", "/recall/threads", query, None)
15
+
16
+ def createThread(self) -> dict[str, Any]:
17
+ return self._transport.request("POST", "/recall/threads", None, None)
18
+
19
+ def getThread(self, thread: str) -> dict[str, Any]:
20
+ return self._transport.request("GET", f"/recall/threads/{thread}", None, None)
21
+
22
+ def deleteThread(self, thread: str) -> dict[str, Any]:
23
+ return self._transport.request(
24
+ "DELETE", f"/recall/threads/{thread}", None, None
25
+ )
26
+
27
+ def search(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
28
+ return self._transport.request("POST", "/recall/search", None, params)
29
+
30
+ def searchFacts(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
31
+ return self._transport.request("POST", "/recall/facts/search", None, params)
32
+
33
+ def listFacts(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
34
+ return self._transport.request("GET", "/recall/facts", query, None)
35
+
36
+ def rememberFact(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
37
+ return self._transport.request("POST", "/recall/facts", None, params)
38
+
39
+ def forgetFact(self, id: str) -> dict[str, Any]:
40
+ return self._transport.request("DELETE", f"/recall/facts/{id}", None, None)
41
+
42
+ def export(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
43
+ return self._transport.request("GET", "/recall/export", query, None)
44
+
45
+ def erase(self) -> dict[str, Any]:
46
+ return self._transport.request("DELETE", "/recall", None, None)
47
+
48
+ def searchDocuments(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
49
+ return self._transport.request("POST", "/recall/documents/search", None, params)
50
+
51
+ def listDocuments(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
52
+ return self._transport.request("GET", "/recall/documents", query, None)
53
+
54
+ def ingestDocument(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
55
+ return self._transport.request("POST", "/recall/documents", None, params)
56
+
57
+ def getDocument(self, document: str) -> dict[str, Any]:
58
+ return self._transport.request(
59
+ "GET", f"/recall/documents/{document}", None, None
60
+ )
61
+
62
+ def deleteDocument(self, document: str) -> dict[str, Any]:
63
+ return self._transport.request(
64
+ "DELETE", f"/recall/documents/{document}", None, None
65
+ )
66
+
67
+ def listCollections(self, query: dict[str, Any] | None = None) -> dict[str, Any]:
68
+ return self._transport.request("GET", "/recall/collections", query, None)
69
+
70
+ def createCollection(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
71
+ return self._transport.request("POST", "/recall/collections", None, params)
72
+
73
+ def getCollection(self, slug: str) -> dict[str, Any]:
74
+ return self._transport.request("GET", f"/recall/collections/{slug}", None, None)
75
+
76
+ def deleteCollection(self, slug: str) -> dict[str, Any]:
77
+ return self._transport.request(
78
+ "DELETE", f"/recall/collections/{slug}", None, None
79
+ )
80
+
81
+ def updateCollection(
82
+ self, slug: str, params: dict[str, Any] | None = None
83
+ ) -> dict[str, Any]:
84
+ return self._transport.request(
85
+ "PATCH", f"/recall/collections/{slug}", None, params
86
+ )