interfaze 1.0.1__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.
- interfaze/__init__.py +69 -0
- interfaze/_chat.py +284 -0
- interfaze/_client.py +108 -0
- interfaze/_constants.py +48 -0
- interfaze/_errors.py +9 -0
- interfaze/_guard.py +21 -0
- interfaze/_inputs.py +112 -0
- interfaze/_schema.py +30 -0
- interfaze/_stream.py +323 -0
- interfaze/_tasks.py +110 -0
- interfaze/_types.py +57 -0
- interfaze/langchain.py +220 -0
- interfaze/py.typed +0 -0
- interfaze-1.0.1.dist-info/METADATA +168 -0
- interfaze-1.0.1.dist-info/RECORD +17 -0
- interfaze-1.0.1.dist-info/WHEEL +4 -0
- interfaze-1.0.1.dist-info/licenses/LICENSE +21 -0
interfaze/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Official Interfaze SDK for Python."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from openai import (
|
|
6
|
+
APIConnectionError,
|
|
7
|
+
APIError,
|
|
8
|
+
APIStatusError,
|
|
9
|
+
APITimeoutError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
BadRequestError,
|
|
12
|
+
ConflictError,
|
|
13
|
+
InternalServerError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
OpenAIError,
|
|
16
|
+
PermissionDeniedError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
UnprocessableEntityError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from . import _inputs as inputs
|
|
22
|
+
from ._client import AsyncInterfaze, Interfaze
|
|
23
|
+
from ._constants import GUARD_CODES, INTERFAZE_BASE_URL, INTERFAZE_MODEL, TASK_NAMES
|
|
24
|
+
from ._errors import InterfazeError
|
|
25
|
+
from ._schema import empty_task_schema, response_format
|
|
26
|
+
from ._stream import AsyncInterfazeStream, InterfazeStream
|
|
27
|
+
from ._types import (
|
|
28
|
+
GuardCode,
|
|
29
|
+
InterfazeChatCompletion,
|
|
30
|
+
Precontext,
|
|
31
|
+
ReasoningEffort,
|
|
32
|
+
TaskName,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = "1.0.1"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"Interfaze",
|
|
39
|
+
"AsyncInterfaze",
|
|
40
|
+
"InterfazeError",
|
|
41
|
+
"InterfazeChatCompletion",
|
|
42
|
+
"InterfazeStream",
|
|
43
|
+
"AsyncInterfazeStream",
|
|
44
|
+
"Precontext",
|
|
45
|
+
"TaskName",
|
|
46
|
+
"GuardCode",
|
|
47
|
+
"ReasoningEffort",
|
|
48
|
+
"response_format",
|
|
49
|
+
"empty_task_schema",
|
|
50
|
+
"inputs",
|
|
51
|
+
"TASK_NAMES",
|
|
52
|
+
"GUARD_CODES",
|
|
53
|
+
"INTERFAZE_MODEL",
|
|
54
|
+
"INTERFAZE_BASE_URL",
|
|
55
|
+
# openai re-exports
|
|
56
|
+
"OpenAIError",
|
|
57
|
+
"APIError",
|
|
58
|
+
"APIStatusError",
|
|
59
|
+
"APIConnectionError",
|
|
60
|
+
"APITimeoutError",
|
|
61
|
+
"BadRequestError",
|
|
62
|
+
"AuthenticationError",
|
|
63
|
+
"NotFoundError",
|
|
64
|
+
"ConflictError",
|
|
65
|
+
"RateLimitError",
|
|
66
|
+
"InternalServerError",
|
|
67
|
+
"PermissionDeniedError",
|
|
68
|
+
"UnprocessableEntityError",
|
|
69
|
+
]
|
interfaze/_chat.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Literal, Optional, Union, cast, overload
|
|
4
|
+
|
|
5
|
+
from openai import AsyncOpenAI, AsyncStream, OpenAI, Stream
|
|
6
|
+
from openai.types.chat import ChatCompletion, ChatCompletionChunk, ParsedChatCompletion
|
|
7
|
+
|
|
8
|
+
from ._constants import INTERFAZE_MODEL
|
|
9
|
+
from ._errors import InterfazeError
|
|
10
|
+
from ._guard import guard_tag
|
|
11
|
+
from ._schema import empty_task_schema
|
|
12
|
+
from ._stream import AsyncInterfazeStream, InterfazeStream, strip_json_fence
|
|
13
|
+
from ._types import GuardCode, InterfazeChatCompletion, TaskName
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_non_empty_schema(rf: Any) -> bool:
|
|
17
|
+
schema = (rf or {}).get("json_schema", {}).get("schema", {}) if isinstance(rf, dict) else {}
|
|
18
|
+
props = schema.get("properties") if isinstance(schema, dict) else None
|
|
19
|
+
return bool(props)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def prepare(
|
|
23
|
+
messages: Iterable[Any],
|
|
24
|
+
model: Optional[str],
|
|
25
|
+
task: Optional[TaskName],
|
|
26
|
+
guard: Optional[List[GuardCode]],
|
|
27
|
+
response_format: Optional[Dict[str, Any]],
|
|
28
|
+
) -> "tuple[List[Any], str, Optional[Dict[str, Any]], bool]":
|
|
29
|
+
rf = response_format
|
|
30
|
+
if task:
|
|
31
|
+
if rf and _is_non_empty_schema(rf):
|
|
32
|
+
raise InterfazeError(
|
|
33
|
+
"A non-empty `response_format` cannot be combined with `task` "
|
|
34
|
+
"(Interfaze runs tasks with raw output)."
|
|
35
|
+
)
|
|
36
|
+
rf = empty_task_schema()
|
|
37
|
+
tags = " ".join(
|
|
38
|
+
t for t in (f"<task>{task}</task>" if task else None, guard_tag(guard) if guard else None) if t
|
|
39
|
+
)
|
|
40
|
+
msgs = list(messages)
|
|
41
|
+
if tags:
|
|
42
|
+
idx = next(
|
|
43
|
+
(i for i, m in enumerate(msgs) if isinstance(m, dict) and m.get("role") == "system"),
|
|
44
|
+
None,
|
|
45
|
+
)
|
|
46
|
+
if idx is not None and isinstance(msgs[idx].get("content"), str):
|
|
47
|
+
merged = dict(msgs[idx])
|
|
48
|
+
existing = merged["content"]
|
|
49
|
+
merged["content"] = f"{tags}\n{existing}" if existing else tags
|
|
50
|
+
msgs[idx] = merged
|
|
51
|
+
else:
|
|
52
|
+
msgs = [{"role": "system", "content": tags}, *msgs]
|
|
53
|
+
strip = isinstance(rf, dict) and rf.get("type") == "json_object"
|
|
54
|
+
return msgs, (model or INTERFAZE_MODEL), rf, strip
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def to_interfaze(raw: ChatCompletion, strip_fence: bool) -> InterfazeChatCompletion:
|
|
58
|
+
data = raw.model_dump()
|
|
59
|
+
if strip_fence:
|
|
60
|
+
try:
|
|
61
|
+
msg = data["choices"][0]["message"]
|
|
62
|
+
if isinstance(msg.get("content"), str):
|
|
63
|
+
msg["content"] = strip_json_fence(msg["content"])
|
|
64
|
+
except (KeyError, IndexError, TypeError):
|
|
65
|
+
pass
|
|
66
|
+
result = InterfazeChatCompletion.model_validate(data)
|
|
67
|
+
request_id = getattr(raw, "_request_id", None)
|
|
68
|
+
if request_id is not None:
|
|
69
|
+
result._request_id = request_id
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _CompletionsBase:
|
|
74
|
+
def _kwargs(
|
|
75
|
+
self,
|
|
76
|
+
messages: Iterable[Any],
|
|
77
|
+
model: Optional[str],
|
|
78
|
+
task: Optional[TaskName],
|
|
79
|
+
guard: Optional[List[GuardCode]],
|
|
80
|
+
response_format: Optional[Dict[str, Any]],
|
|
81
|
+
extra: Dict[str, Any],
|
|
82
|
+
) -> "tuple[Dict[str, Any], bool]":
|
|
83
|
+
msgs, mdl, rf, strip = prepare(messages, model, task, guard, response_format)
|
|
84
|
+
kw: Dict[str, Any] = {"model": mdl, "messages": msgs, **extra}
|
|
85
|
+
if rf is not None:
|
|
86
|
+
kw["response_format"] = rf
|
|
87
|
+
return kw, strip
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Completions(_CompletionsBase):
|
|
91
|
+
"""`interfaze.chat.completions` (sync)."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, client: OpenAI) -> None:
|
|
94
|
+
self._client = client
|
|
95
|
+
|
|
96
|
+
@overload
|
|
97
|
+
def create(
|
|
98
|
+
self,
|
|
99
|
+
*,
|
|
100
|
+
messages: Iterable[Any],
|
|
101
|
+
stream: Literal[False] = False,
|
|
102
|
+
model: str = ...,
|
|
103
|
+
task: Optional[TaskName] = ...,
|
|
104
|
+
guard: Optional[List[GuardCode]] = ...,
|
|
105
|
+
response_format: Optional[Dict[str, Any]] = ...,
|
|
106
|
+
**kwargs: Any,
|
|
107
|
+
) -> InterfazeChatCompletion: ...
|
|
108
|
+
@overload
|
|
109
|
+
def create(
|
|
110
|
+
self,
|
|
111
|
+
*,
|
|
112
|
+
messages: Iterable[Any],
|
|
113
|
+
stream: Literal[True],
|
|
114
|
+
model: str = ...,
|
|
115
|
+
task: Optional[TaskName] = ...,
|
|
116
|
+
guard: Optional[List[GuardCode]] = ...,
|
|
117
|
+
response_format: Optional[Dict[str, Any]] = ...,
|
|
118
|
+
**kwargs: Any,
|
|
119
|
+
) -> Stream[ChatCompletionChunk]: ...
|
|
120
|
+
|
|
121
|
+
def create(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
messages: Iterable[Any],
|
|
125
|
+
stream: bool = False,
|
|
126
|
+
model: str = INTERFAZE_MODEL,
|
|
127
|
+
task: Optional[TaskName] = None,
|
|
128
|
+
guard: Optional[List[GuardCode]] = None,
|
|
129
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
130
|
+
**kwargs: Any,
|
|
131
|
+
) -> "Union[InterfazeChatCompletion, Stream[ChatCompletionChunk]]":
|
|
132
|
+
kw, strip = self._kwargs(messages, model, task, guard, response_format, kwargs)
|
|
133
|
+
if stream:
|
|
134
|
+
return cast(
|
|
135
|
+
"Stream[ChatCompletionChunk]", self._client.chat.completions.create(stream=True, **kw)
|
|
136
|
+
)
|
|
137
|
+
raw = self._client.chat.completions.create(**kw)
|
|
138
|
+
return to_interfaze(cast(ChatCompletion, raw), strip)
|
|
139
|
+
|
|
140
|
+
def stream(
|
|
141
|
+
self,
|
|
142
|
+
*,
|
|
143
|
+
messages: Iterable[Any],
|
|
144
|
+
model: str = INTERFAZE_MODEL,
|
|
145
|
+
task: Optional[TaskName] = None,
|
|
146
|
+
guard: Optional[List[GuardCode]] = None,
|
|
147
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
148
|
+
**kwargs: Any,
|
|
149
|
+
) -> InterfazeStream:
|
|
150
|
+
kw, strip = self._kwargs(messages, model, task, guard, response_format, kwargs)
|
|
151
|
+
return InterfazeStream(self._client, kw, strip)
|
|
152
|
+
|
|
153
|
+
def parse(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
messages: Iterable[Any],
|
|
157
|
+
response_format: Any,
|
|
158
|
+
model: str = INTERFAZE_MODEL,
|
|
159
|
+
guard: Optional[List[GuardCode]] = None,
|
|
160
|
+
**kwargs: Any,
|
|
161
|
+
) -> "ParsedChatCompletion[Any]":
|
|
162
|
+
"""Structured output via a Pydantic model (delegates to the OpenAI client's ``parse``).
|
|
163
|
+
|
|
164
|
+
Interfaze extras (``vcache``/``precontext``) are present but untyped here; use ``create`` for
|
|
165
|
+
the typed extended completion.
|
|
166
|
+
"""
|
|
167
|
+
msgs = prepare(messages, model, None, guard, None)[0]
|
|
168
|
+
return self._client.chat.completions.parse(
|
|
169
|
+
model=model, messages=msgs, response_format=response_format, **kwargs
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def with_raw_response(self) -> Any:
|
|
174
|
+
"""Raw-HTTP escape hatch (delegates to the OpenAI client; bypasses task/guard preprocessing)."""
|
|
175
|
+
return self._client.chat.completions.with_raw_response
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def with_streaming_response(self) -> Any:
|
|
179
|
+
"""Streaming raw-HTTP escape hatch (delegates to the OpenAI client)."""
|
|
180
|
+
return self._client.chat.completions.with_streaming_response
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class AsyncCompletions(_CompletionsBase):
|
|
184
|
+
"""`interfaze.chat.completions` (async)."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, client: AsyncOpenAI) -> None:
|
|
187
|
+
self._client = client
|
|
188
|
+
|
|
189
|
+
@overload
|
|
190
|
+
async def create(
|
|
191
|
+
self,
|
|
192
|
+
*,
|
|
193
|
+
messages: Iterable[Any],
|
|
194
|
+
stream: Literal[False] = False,
|
|
195
|
+
model: str = ...,
|
|
196
|
+
task: Optional[TaskName] = ...,
|
|
197
|
+
guard: Optional[List[GuardCode]] = ...,
|
|
198
|
+
response_format: Optional[Dict[str, Any]] = ...,
|
|
199
|
+
**kwargs: Any,
|
|
200
|
+
) -> InterfazeChatCompletion: ...
|
|
201
|
+
@overload
|
|
202
|
+
async def create(
|
|
203
|
+
self,
|
|
204
|
+
*,
|
|
205
|
+
messages: Iterable[Any],
|
|
206
|
+
stream: Literal[True],
|
|
207
|
+
model: str = ...,
|
|
208
|
+
task: Optional[TaskName] = ...,
|
|
209
|
+
guard: Optional[List[GuardCode]] = ...,
|
|
210
|
+
response_format: Optional[Dict[str, Any]] = ...,
|
|
211
|
+
**kwargs: Any,
|
|
212
|
+
) -> AsyncStream[ChatCompletionChunk]: ...
|
|
213
|
+
|
|
214
|
+
async def create(
|
|
215
|
+
self,
|
|
216
|
+
*,
|
|
217
|
+
messages: Iterable[Any],
|
|
218
|
+
stream: bool = False,
|
|
219
|
+
model: str = INTERFAZE_MODEL,
|
|
220
|
+
task: Optional[TaskName] = None,
|
|
221
|
+
guard: Optional[List[GuardCode]] = None,
|
|
222
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
223
|
+
**kwargs: Any,
|
|
224
|
+
) -> "Union[InterfazeChatCompletion, AsyncStream[ChatCompletionChunk]]":
|
|
225
|
+
kw, strip = self._kwargs(messages, model, task, guard, response_format, kwargs)
|
|
226
|
+
if stream:
|
|
227
|
+
return cast(
|
|
228
|
+
"AsyncStream[ChatCompletionChunk]",
|
|
229
|
+
await self._client.chat.completions.create(stream=True, **kw),
|
|
230
|
+
)
|
|
231
|
+
raw = await self._client.chat.completions.create(**kw)
|
|
232
|
+
return to_interfaze(cast(ChatCompletion, raw), strip)
|
|
233
|
+
|
|
234
|
+
def stream(
|
|
235
|
+
self,
|
|
236
|
+
*,
|
|
237
|
+
messages: Iterable[Any],
|
|
238
|
+
model: str = INTERFAZE_MODEL,
|
|
239
|
+
task: Optional[TaskName] = None,
|
|
240
|
+
guard: Optional[List[GuardCode]] = None,
|
|
241
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
242
|
+
**kwargs: Any,
|
|
243
|
+
) -> AsyncInterfazeStream:
|
|
244
|
+
kw, strip = self._kwargs(messages, model, task, guard, response_format, kwargs)
|
|
245
|
+
return AsyncInterfazeStream(self._client, kw, strip)
|
|
246
|
+
|
|
247
|
+
async def parse(
|
|
248
|
+
self,
|
|
249
|
+
*,
|
|
250
|
+
messages: Iterable[Any],
|
|
251
|
+
response_format: Any,
|
|
252
|
+
model: str = INTERFAZE_MODEL,
|
|
253
|
+
guard: Optional[List[GuardCode]] = None,
|
|
254
|
+
**kwargs: Any,
|
|
255
|
+
) -> "ParsedChatCompletion[Any]":
|
|
256
|
+
"""Structured output via a Pydantic model (delegates to the OpenAI client's ``parse``).
|
|
257
|
+
|
|
258
|
+
Interfaze extras (``vcache``/``precontext``) are present but untyped here; use ``create`` for
|
|
259
|
+
the typed extended completion.
|
|
260
|
+
"""
|
|
261
|
+
msgs = prepare(messages, model, None, guard, None)[0]
|
|
262
|
+
return await self._client.chat.completions.parse(
|
|
263
|
+
model=model, messages=msgs, response_format=response_format, **kwargs
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def with_raw_response(self) -> Any:
|
|
268
|
+
"""Raw-HTTP escape hatch (delegates to the OpenAI client; bypasses task/guard preprocessing)."""
|
|
269
|
+
return self._client.chat.completions.with_raw_response
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def with_streaming_response(self) -> Any:
|
|
273
|
+
"""Streaming raw-HTTP escape hatch (delegates to the OpenAI client)."""
|
|
274
|
+
return self._client.chat.completions.with_streaming_response
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class Chat:
|
|
278
|
+
def __init__(self, client: OpenAI) -> None:
|
|
279
|
+
self.completions = Completions(client)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class AsyncChat:
|
|
283
|
+
def __init__(self, client: AsyncOpenAI) -> None:
|
|
284
|
+
self.completions = AsyncCompletions(client)
|
interfaze/_client.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from openai import AsyncOpenAI, OpenAI
|
|
7
|
+
|
|
8
|
+
from ._chat import AsyncChat, Chat
|
|
9
|
+
from ._constants import (
|
|
10
|
+
DEFAULT_TIMEOUT,
|
|
11
|
+
HEADER_ADMIN_KEY,
|
|
12
|
+
HEADER_BYPASS_CACHE,
|
|
13
|
+
HEADER_BYPASS_MOE,
|
|
14
|
+
HEADER_SHOW_ADDITIONAL_INFO,
|
|
15
|
+
INTERFAZE_BASE_URL,
|
|
16
|
+
)
|
|
17
|
+
from ._errors import InterfazeError
|
|
18
|
+
from ._tasks import AsyncTasks, Tasks
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve_key(api_key: Optional[str]) -> str:
|
|
22
|
+
key = api_key or os.environ.get("INTERFAZE_API_KEY")
|
|
23
|
+
if not key:
|
|
24
|
+
raise InterfazeError(
|
|
25
|
+
"Missing API key. Pass Interfaze(api_key=...) or set the INTERFAZE_API_KEY environment variable."
|
|
26
|
+
)
|
|
27
|
+
return key
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_headers(
|
|
31
|
+
default_headers: Optional[Dict[str, str]],
|
|
32
|
+
show_additional_info: bool,
|
|
33
|
+
bypass_moe: bool,
|
|
34
|
+
bypass_cache: bool,
|
|
35
|
+
admin_key: Optional[str],
|
|
36
|
+
) -> Dict[str, str]:
|
|
37
|
+
headers: Dict[str, str] = dict(default_headers or {})
|
|
38
|
+
if show_additional_info:
|
|
39
|
+
headers[HEADER_SHOW_ADDITIONAL_INFO] = "true"
|
|
40
|
+
if bypass_moe:
|
|
41
|
+
headers[HEADER_BYPASS_MOE] = "true"
|
|
42
|
+
if bypass_cache:
|
|
43
|
+
headers[HEADER_BYPASS_CACHE] = "true"
|
|
44
|
+
if admin_key:
|
|
45
|
+
headers[HEADER_ADMIN_KEY] = admin_key
|
|
46
|
+
return headers
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Interfaze:
|
|
50
|
+
"""Synchronous Interfaze client — a curated wrapper over ``openai.OpenAI``.
|
|
51
|
+
|
|
52
|
+
Exposes the endpoints Interfaze implements (``chat.completions``, ``models``) plus task
|
|
53
|
+
helpers (``tasks.*``). The underlying client is available at ``.openai``.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
api_key: Optional[str] = None,
|
|
60
|
+
base_url: Optional[str] = None,
|
|
61
|
+
show_additional_info: bool = False,
|
|
62
|
+
bypass_moe: bool = False,
|
|
63
|
+
bypass_cache: bool = False,
|
|
64
|
+
admin_key: Optional[str] = None,
|
|
65
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
) -> None:
|
|
68
|
+
kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
|
|
69
|
+
self.openai = OpenAI(
|
|
70
|
+
api_key=_resolve_key(api_key),
|
|
71
|
+
base_url=base_url or INTERFAZE_BASE_URL,
|
|
72
|
+
default_headers=_build_headers(
|
|
73
|
+
default_headers, show_additional_info, bypass_moe, bypass_cache, admin_key
|
|
74
|
+
),
|
|
75
|
+
**kwargs,
|
|
76
|
+
)
|
|
77
|
+
self.chat = Chat(self.openai)
|
|
78
|
+
self.models = self.openai.models
|
|
79
|
+
self.tasks = Tasks(self.chat.completions)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AsyncInterfaze:
|
|
83
|
+
"""Asynchronous Interfaze client — a curated wrapper over ``openai.AsyncOpenAI``."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
api_key: Optional[str] = None,
|
|
89
|
+
base_url: Optional[str] = None,
|
|
90
|
+
show_additional_info: bool = False,
|
|
91
|
+
bypass_moe: bool = False,
|
|
92
|
+
bypass_cache: bool = False,
|
|
93
|
+
admin_key: Optional[str] = None,
|
|
94
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
95
|
+
**kwargs: Any,
|
|
96
|
+
) -> None:
|
|
97
|
+
kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
|
|
98
|
+
self.openai = AsyncOpenAI(
|
|
99
|
+
api_key=_resolve_key(api_key),
|
|
100
|
+
base_url=base_url or INTERFAZE_BASE_URL,
|
|
101
|
+
default_headers=_build_headers(
|
|
102
|
+
default_headers, show_additional_info, bypass_moe, bypass_cache, admin_key
|
|
103
|
+
),
|
|
104
|
+
**kwargs,
|
|
105
|
+
)
|
|
106
|
+
self.chat = AsyncChat(self.openai)
|
|
107
|
+
self.models = self.openai.models
|
|
108
|
+
self.tasks = AsyncTasks(self.chat.completions)
|
interfaze/_constants.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
INTERFAZE_BASE_URL = "https://api.interfaze.ai/v1"
|
|
4
|
+
INTERFAZE_MODEL = "interfaze-beta"
|
|
5
|
+
DEFAULT_TIMEOUT = 900.0
|
|
6
|
+
|
|
7
|
+
# Task names accepted in a <task>...</task> tag.
|
|
8
|
+
TASK_NAMES = (
|
|
9
|
+
"ocr",
|
|
10
|
+
"object_detection",
|
|
11
|
+
"gui_detection",
|
|
12
|
+
"web_search",
|
|
13
|
+
"scraper",
|
|
14
|
+
"translate",
|
|
15
|
+
"speech_to_text",
|
|
16
|
+
"forecast",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Guardrail categories (ALL enables everything).
|
|
20
|
+
GUARD_CODES = (
|
|
21
|
+
"S1",
|
|
22
|
+
"S2",
|
|
23
|
+
"S3",
|
|
24
|
+
"S4",
|
|
25
|
+
"S5",
|
|
26
|
+
"S6",
|
|
27
|
+
"S7",
|
|
28
|
+
"S8",
|
|
29
|
+
"S9",
|
|
30
|
+
"S10",
|
|
31
|
+
"S11",
|
|
32
|
+
"S12",
|
|
33
|
+
"S13",
|
|
34
|
+
"S14",
|
|
35
|
+
"S1_IMAGE",
|
|
36
|
+
"S12_IMAGE",
|
|
37
|
+
"S15_IMAGE",
|
|
38
|
+
"ALL",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Formats Interfaze rejects.
|
|
42
|
+
BLACKLISTED_FORMATS = ("image/gif", "image/avif")
|
|
43
|
+
|
|
44
|
+
# Interfaze control-plane headers.
|
|
45
|
+
HEADER_SHOW_ADDITIONAL_INFO = "x-show-additional-info"
|
|
46
|
+
HEADER_BYPASS_MOE = "x-bypass-moe"
|
|
47
|
+
HEADER_BYPASS_CACHE = "x-bypass-cache"
|
|
48
|
+
HEADER_ADMIN_KEY = "x-admin-key"
|
interfaze/_errors.py
ADDED
interfaze/_guard.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ._constants import GUARD_CODES
|
|
6
|
+
from ._errors import InterfazeError
|
|
7
|
+
from ._types import GuardCode
|
|
8
|
+
|
|
9
|
+
_VALID = set(GUARD_CODES)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def guard_tag(codes: List[GuardCode]) -> str:
|
|
13
|
+
"""Serialize guardrail categories into the ``<guard>...</guard>`` tag, validating codes."""
|
|
14
|
+
if not codes:
|
|
15
|
+
raise InterfazeError("`guard` must contain at least one code")
|
|
16
|
+
invalid = [c for c in codes if c not in _VALID]
|
|
17
|
+
if invalid:
|
|
18
|
+
raise InterfazeError(
|
|
19
|
+
f"Invalid guard code(s): {', '.join(invalid)}. Valid codes: {', '.join(GUARD_CODES)}"
|
|
20
|
+
)
|
|
21
|
+
return f"<guard>{', '.join(codes)}</guard>"
|
interfaze/_inputs.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64 as _base64
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Optional, Union
|
|
6
|
+
|
|
7
|
+
from ._constants import BLACKLISTED_FORMATS
|
|
8
|
+
from ._errors import InterfazeError
|
|
9
|
+
|
|
10
|
+
BytesLike = Union[bytes, bytearray]
|
|
11
|
+
|
|
12
|
+
_EXT_MIME = {
|
|
13
|
+
"png": "image/png",
|
|
14
|
+
"jpg": "image/jpeg",
|
|
15
|
+
"jpeg": "image/jpeg",
|
|
16
|
+
"webp": "image/webp",
|
|
17
|
+
"gif": "image/gif",
|
|
18
|
+
"bmp": "image/bmp",
|
|
19
|
+
"heic": "image/heic",
|
|
20
|
+
"heif": "image/heif",
|
|
21
|
+
"pdf": "application/pdf",
|
|
22
|
+
"csv": "text/csv",
|
|
23
|
+
"tsv": "text/tab-separated-values",
|
|
24
|
+
"xml": "application/xml",
|
|
25
|
+
"json": "application/json",
|
|
26
|
+
"txt": "text/plain",
|
|
27
|
+
"md": "text/markdown",
|
|
28
|
+
"markdown": "text/markdown",
|
|
29
|
+
"yaml": "application/yaml",
|
|
30
|
+
"yml": "application/yaml",
|
|
31
|
+
"wav": "audio/wav",
|
|
32
|
+
"mp3": "audio/mpeg",
|
|
33
|
+
"m4a": "audio/mp4",
|
|
34
|
+
"ogg": "audio/ogg",
|
|
35
|
+
"flac": "audio/flac",
|
|
36
|
+
"mp4": "video/mp4",
|
|
37
|
+
"mov": "video/quicktime",
|
|
38
|
+
"webm": "video/webm",
|
|
39
|
+
"avi": "video/x-msvideo",
|
|
40
|
+
"mkv": "video/x-matroska",
|
|
41
|
+
"3gp": "video/3gpp",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _mime_from_data_url(s: str) -> Optional[str]:
|
|
46
|
+
if s.startswith("data:"):
|
|
47
|
+
return s[5:].split(";")[0].split(",")[0] or None
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _ext_of(url_or_name: str) -> Optional[str]:
|
|
52
|
+
base = url_or_name.split("?")[0].split("#")[0]
|
|
53
|
+
return base.rsplit(".", 1)[-1].lower() if "." in base else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _assert_allowed(mime: Optional[str]) -> None:
|
|
57
|
+
if mime and mime in BLACKLISTED_FORMATS:
|
|
58
|
+
raise InterfazeError(f'Format "{mime}" is not supported by Interfaze.')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def data_url(data: BytesLike, mime_type: str) -> str:
|
|
62
|
+
"""Build a base64 ``data:`` URI from raw bytes."""
|
|
63
|
+
_assert_allowed(mime_type)
|
|
64
|
+
return f"data:{mime_type};base64,{_base64.b64encode(bytes(data)).decode('ascii')}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def from_path(path: Union[str, Path]) -> str:
|
|
68
|
+
"""Read a local file into a ``data:`` URI (mime by extension)."""
|
|
69
|
+
p = Path(path)
|
|
70
|
+
mime = _EXT_MIME.get((p.suffix.lstrip(".")).lower(), "application/octet-stream")
|
|
71
|
+
return data_url(p.read_bytes(), mime)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def image(src: str) -> Dict[str, Any]:
|
|
75
|
+
"""Image content part. ``src`` = https URL or ``data:`` URI."""
|
|
76
|
+
_assert_allowed(_mime_from_data_url(src) or _EXT_MIME.get(_ext_of(src) or ""))
|
|
77
|
+
return {"type": "image_url", "image_url": {"url": src}}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def file(src: str, *, filename: Optional[str] = None, format: Optional[str] = None) -> Dict[str, Any]:
|
|
81
|
+
"""File content part (pdf/csv/xml/json/text/audio/video/…). ``src`` = https URL or ``data:`` URI."""
|
|
82
|
+
mime = format or _mime_from_data_url(src) or _EXT_MIME.get(_ext_of(filename or src) or "")
|
|
83
|
+
_assert_allowed(mime)
|
|
84
|
+
f: Dict[str, Any] = {"file_data": src}
|
|
85
|
+
if filename:
|
|
86
|
+
f["filename"] = filename
|
|
87
|
+
if mime:
|
|
88
|
+
f["format"] = mime
|
|
89
|
+
return {"type": "file", "file": f}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def audio(src: str, *, format: Optional[str] = None) -> Dict[str, Any]:
|
|
93
|
+
"""Audio content part via ``input_audio`` (``audio_url`` is a dead field in Interfaze)."""
|
|
94
|
+
mime = _mime_from_data_url(src)
|
|
95
|
+
_assert_allowed(mime or _EXT_MIME.get(_ext_of(src) or ""))
|
|
96
|
+
fmt = format or (mime.split("/", 1)[-1] if mime else _ext_of(src)) or "wav"
|
|
97
|
+
return {"type": "input_audio", "input_audio": {"data": src, "format": fmt}}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def video(src: str, *, filename: Optional[str] = None) -> Dict[str, Any]:
|
|
101
|
+
"""Video content part — rides on the ``file`` part (there is no video content part)."""
|
|
102
|
+
return file(src, filename=filename)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def auto_part(src: str, *, filename: Optional[str] = None, format: Optional[str] = None) -> Dict[str, Any]:
|
|
106
|
+
"""Pick a content part by media type: image → image_url, audio → input_audio, else file."""
|
|
107
|
+
mime = format or _mime_from_data_url(src) or _EXT_MIME.get(_ext_of(filename or src) or "")
|
|
108
|
+
if mime and mime.startswith("image/"):
|
|
109
|
+
return image(src)
|
|
110
|
+
if mime and mime.startswith("audio/"):
|
|
111
|
+
return audio(src, format=format or mime.split("/", 1)[-1])
|
|
112
|
+
return file(src, filename=filename, format=format)
|
interfaze/_schema.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
JSONSchema = Dict[str, Any]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def empty_task_schema(name: str = "empty_schema") -> Dict[str, Any]:
|
|
9
|
+
"""Empty ``response_format`` for raw ``<task>`` runs."""
|
|
10
|
+
return {"type": "json_schema", "json_schema": {"name": name, "schema": {}}}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def response_format(schema: JSONSchema, name: str = "response") -> Dict[str, Any]:
|
|
14
|
+
"""Build a structured-output ``response_format`` from a JSON Schema.
|
|
15
|
+
|
|
16
|
+
Non-object roots are wrapped in a ``{"result": ...}`` object (structured output requires
|
|
17
|
+
an object root); the wrapped output is then under the ``result`` key.
|
|
18
|
+
"""
|
|
19
|
+
return {"type": "json_schema", "json_schema": {"name": name, "schema": _ensure_object_root(schema)}}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ensure_object_root(schema: JSONSchema) -> JSONSchema:
|
|
23
|
+
if isinstance(schema, dict) and schema.get("type") == "object":
|
|
24
|
+
return schema
|
|
25
|
+
return {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {"result": schema},
|
|
28
|
+
"required": ["result"],
|
|
29
|
+
"additionalProperties": False,
|
|
30
|
+
}
|