ocai 0.1.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.
- ocai/chat/__init__.py +271 -0
- ocai/chat/canonical.py +37 -0
- ocai/chat/facade.py +1572 -0
- ocai/chat/protocol.py +128 -0
- ocai/chat/py.typed +0 -0
- ocai/chat/types/__init__.py +195 -0
- ocai/chat/types/annotations.py +237 -0
- ocai/chat/types/config.py +336 -0
- ocai/chat/types/finish.py +36 -0
- ocai/chat/types/message.py +198 -0
- ocai/chat/types/parts.py +821 -0
- ocai/chat/types/request.py +124 -0
- ocai/chat/types/response.py +145 -0
- ocai/chat/types/role.py +90 -0
- ocai/chat/types/stream.py +1203 -0
- ocai/chat/types/tools.py +467 -0
- ocai/chat/types/usage.py +22 -0
- ocai/convert/__init__.py +89 -0
- ocai/convert/catalog.py +1029 -0
- ocai/convert/conformance.py +800 -0
- ocai/convert/contract.py +403 -0
- ocai/convert/graph.py +625 -0
- ocai/convert/hosts.py +70 -0
- ocai/convert/inspection.py +68 -0
- ocai/convert/migrate.py +69 -0
- ocai/convert/py.typed +0 -0
- ocai/convert/resources.py +148 -0
- ocai/core/__init__.py +212 -0
- ocai/core/_headers.py +45 -0
- ocai/core/canonjson.py +156 -0
- ocai/core/credentials.py +177 -0
- ocai/core/errors.py +190 -0
- ocai/core/fidelity.py +624 -0
- ocai/core/finish.py +396 -0
- ocai/core/identity.py +213 -0
- ocai/core/ir.py +328 -0
- ocai/core/jsonschema.py +326 -0
- ocai/core/primitives.py +114 -0
- ocai/core/profile.py +154 -0
- ocai/core/py.typed +0 -0
- ocai/core/refs.py +168 -0
- ocai/core/result.py +135 -0
- ocai/core/sources.py +251 -0
- ocai/core/traits.py +171 -0
- ocai/core/usage.py +246 -0
- ocai/embedding/__init__.py +28 -0
- ocai/embedding/protocol.py +77 -0
- ocai/embedding/py.typed +0 -0
- ocai/embedding/types/__init__.py +26 -0
- ocai/embedding/types/document.py +77 -0
- ocai/embedding/types/embedding.py +78 -0
- ocai/embedding/types/request.py +124 -0
- ocai/embedding/types/response.py +64 -0
- ocai/runtime/__init__.py +75 -0
- ocai/runtime/binding.py +120 -0
- ocai/runtime/deadline.py +58 -0
- ocai/runtime/decisions.py +217 -0
- ocai/runtime/drivers.py +772 -0
- ocai/runtime/dual.py +317 -0
- ocai/runtime/interceptors.py +307 -0
- ocai/runtime/observe.py +193 -0
- ocai/runtime/py.typed +0 -0
- ocai/vectorstore/__init__.py +26 -0
- ocai/vectorstore/protocol.py +255 -0
- ocai/vectorstore/py.typed +0 -0
- ocai/vectorstore/types/__init__.py +22 -0
- ocai/vectorstore/types/filter.py +14 -0
- ocai/vectorstore/types/record.py +106 -0
- ocai/wire/__init__.py +67 -0
- ocai/wire/framing.py +138 -0
- ocai/wire/hosting.py +497 -0
- ocai/wire/http.py +118 -0
- ocai/wire/policies.py +188 -0
- ocai/wire/py.typed +0 -0
- ocai/wire/sse.py +144 -0
- ocai/wire/transport.py +385 -0
- ocai-0.1.1.dist-info/METADATA +83 -0
- ocai-0.1.1.dist-info/RECORD +79 -0
- ocai-0.1.1.dist-info/WHEEL +4 -0
ocai/chat/__init__.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""`ocai.chat` — the chat capability contract (architecture §3).
|
|
2
|
+
|
|
3
|
+
The port (`ChatModel`), the canonical chat IR (`ocai.chat.types`), the
|
|
4
|
+
canonical graph nodes (`ocai.chat.canonical`), the wire-backed facade
|
|
5
|
+
(`WireChatModel`), and the §3.4 front door (`model`). Everything except
|
|
6
|
+
`facade.py` depends on `ocai.core` only; `facade.py` is the one designated
|
|
7
|
+
module that composes convert/runtime/wire behind the port (named `facade`
|
|
8
|
+
so the `ocai.chat.model()` front-door factory is never shadowed by a
|
|
9
|
+
module — the factory below composes lazily, inside the call, through the
|
|
10
|
+
same sanctioned seams).
|
|
11
|
+
|
|
12
|
+
The curated exports below are the user-facing surface: build a model with
|
|
13
|
+
`model("openai/gpt-5", credentials=...)`, build a `ChatRequest` from
|
|
14
|
+
`Message`s and `Tool`s, call any `ChatModel`, receive a
|
|
15
|
+
`CallResult[ChatResponse]` or a `ChatStream` of `ChatChunk`s.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from ocai.chat.canonical import CANONICAL_CHAT_CHUNK
|
|
23
|
+
from ocai.chat.canonical import CANONICAL_CHAT_REQUEST
|
|
24
|
+
from ocai.chat.canonical import CANONICAL_CHAT_RESPONSE
|
|
25
|
+
from ocai.chat.canonical import OCAI_INTERFACE
|
|
26
|
+
from ocai.chat.canonical import REVISION
|
|
27
|
+
from ocai.chat.facade import WireChatModel
|
|
28
|
+
from ocai.chat.protocol import ChatModel
|
|
29
|
+
from ocai.chat.types import AsyncChatStream
|
|
30
|
+
from ocai.chat.types import ChatChunk
|
|
31
|
+
from ocai.chat.types import ChatConfig
|
|
32
|
+
from ocai.chat.types import ChatRequest
|
|
33
|
+
from ocai.chat.types import ChatResponse
|
|
34
|
+
from ocai.chat.types import ChatStream
|
|
35
|
+
from ocai.chat.types import FinishInfo
|
|
36
|
+
from ocai.chat.types import Message
|
|
37
|
+
from ocai.chat.types import Part
|
|
38
|
+
from ocai.chat.types import Role
|
|
39
|
+
from ocai.chat.types import ServerTool
|
|
40
|
+
from ocai.chat.types import ServerToolLocation
|
|
41
|
+
from ocai.chat.types import StreamAccumulator
|
|
42
|
+
from ocai.chat.types import StreamEnd
|
|
43
|
+
from ocai.chat.types import StreamOutcome
|
|
44
|
+
from ocai.chat.types import Tool
|
|
45
|
+
from ocai.chat.types import ToolChoice
|
|
46
|
+
from ocai.chat.types import ToolDefinition
|
|
47
|
+
from ocai.chat.types import ToolUnknown
|
|
48
|
+
from ocai.chat.types import Usage
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from collections.abc import Sequence
|
|
52
|
+
|
|
53
|
+
from ocai.chat.facade import InterceptedChatModel
|
|
54
|
+
from ocai.chat.facade import ObservedChatModel
|
|
55
|
+
from ocai.convert.catalog import Catalog
|
|
56
|
+
from ocai.core.credentials import Credential
|
|
57
|
+
from ocai.core.fidelity import FidelityPolicy
|
|
58
|
+
from ocai.core.fidelity import Route
|
|
59
|
+
from ocai.runtime.decisions import WirePolicy
|
|
60
|
+
from ocai.runtime.interceptors import CallInterceptor
|
|
61
|
+
from ocai.runtime.observe import Observer
|
|
62
|
+
from ocai.wire.hosting import Host
|
|
63
|
+
from ocai.wire.transport import SyncTransport
|
|
64
|
+
from ocai.wire.transport import Transport
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"CANONICAL_CHAT_CHUNK",
|
|
68
|
+
"CANONICAL_CHAT_REQUEST",
|
|
69
|
+
"CANONICAL_CHAT_RESPONSE",
|
|
70
|
+
"OCAI_INTERFACE",
|
|
71
|
+
"REVISION",
|
|
72
|
+
"AsyncChatStream",
|
|
73
|
+
"ChatChunk",
|
|
74
|
+
"ChatConfig",
|
|
75
|
+
"ChatModel",
|
|
76
|
+
"ChatRequest",
|
|
77
|
+
"ChatResponse",
|
|
78
|
+
"ChatStream",
|
|
79
|
+
"FinishInfo",
|
|
80
|
+
"Message",
|
|
81
|
+
"Part",
|
|
82
|
+
"Role",
|
|
83
|
+
"StreamAccumulator",
|
|
84
|
+
"StreamEnd",
|
|
85
|
+
"StreamOutcome",
|
|
86
|
+
"ServerTool",
|
|
87
|
+
"ServerToolLocation",
|
|
88
|
+
"ToolUnknown",
|
|
89
|
+
"ToolDefinition",
|
|
90
|
+
"Tool",
|
|
91
|
+
"ToolChoice",
|
|
92
|
+
"Usage",
|
|
93
|
+
"WireChatModel",
|
|
94
|
+
"model",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def model(
|
|
99
|
+
spec: str,
|
|
100
|
+
*,
|
|
101
|
+
credentials: Credential,
|
|
102
|
+
host: Host | str | None = None,
|
|
103
|
+
catalog: Catalog | None = None,
|
|
104
|
+
transport: Transport | SyncTransport | None = None,
|
|
105
|
+
interceptors: Sequence[CallInterceptor] = (),
|
|
106
|
+
policies: Sequence[WirePolicy] = (),
|
|
107
|
+
observers: Sequence[Observer] = (),
|
|
108
|
+
conversion_policy: FidelityPolicy | None = None,
|
|
109
|
+
interface: str | None = None,
|
|
110
|
+
tag: str | None = None,
|
|
111
|
+
) -> WireChatModel | InterceptedChatModel | ObservedChatModel:
|
|
112
|
+
"""Build a chat model from one spec string — the §3.4 core front door.
|
|
113
|
+
|
|
114
|
+
The whole provider+model choice is ONE data value
|
|
115
|
+
(`model(os.environ["OCAI_MODEL"], ...)` — switching providers is a
|
|
116
|
+
deployment change, zero code):
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import ocai.chat
|
|
120
|
+
from ocai.core import ApiKey
|
|
121
|
+
|
|
122
|
+
chat = ocai.chat.model("openai/gpt-5", credentials=ApiKey.from_env("OPENAI_API_KEY"))
|
|
123
|
+
res = chat.chat(ChatRequest(messages=[Message.user("hello")]))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The factory resolves the provider against the catalog, builds the codec
|
|
127
|
+
from the serving fragment's registered component factory, composes the
|
|
128
|
+
`Binding` internally (a `Direct` host at the fragment's declared
|
|
129
|
+
first-party origin unless `host` is passed), and binds the model name to
|
|
130
|
+
the returned object — requests stay pure conversation. Unpinned
|
|
131
|
+
resolution considers only interfaces the catalog can actually *serve*
|
|
132
|
+
(those with a registered chat component); a nodes-only stub interface is
|
|
133
|
+
reachable by explicit `interface=` pin only.
|
|
134
|
+
|
|
135
|
+
Everything composed here is reachable by hand (`WireChatModel` + a
|
|
136
|
+
`Binding` + a `Catalog`); this door adds nothing a power user cannot
|
|
137
|
+
build — and the plugin `chat_model` helpers are sugar over exactly this
|
|
138
|
+
path, never a separate one.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
spec: The model spec `"<provider>/<model>"` (the LiteLLM/OpenRouter
|
|
142
|
+
convention). Split on the FIRST slash only — the model part may
|
|
143
|
+
itself contain slashes (`"openai/meta-llama/Llama-3.3-70B"` on a
|
|
144
|
+
vLLM gateway). The spec never carries an interface; pinning is
|
|
145
|
+
the `interface`/`tag` kwargs.
|
|
146
|
+
credentials: The credential the driver applies at the leaf, per
|
|
147
|
+
attempt. Required — there is no ambient discovery (rule 13).
|
|
148
|
+
host: A hosting target, a host name explicitly declared in the catalog,
|
|
149
|
+
or None for first-party `Direct` at the dialect's declared origin.
|
|
150
|
+
catalog: The governed conversion catalog, or None for the
|
|
151
|
+
entry-point-discovered `default_catalog()`.
|
|
152
|
+
transport: The wire transport, or None for a fresh `HttpxTransport`.
|
|
153
|
+
interceptors: The port-tier chain wrapped around the facade,
|
|
154
|
+
outermost first; empty for the bare facade.
|
|
155
|
+
conversion_policy: Caller-defined acceptance over field losses and shifts.
|
|
156
|
+
observers: Authorized passive metadata-only lifecycle observers.
|
|
157
|
+
policies: The wire-tier hooks (retry, rate-limit, wiretap) the
|
|
158
|
+
drivers consult on every attempt, in order.
|
|
159
|
+
interface: An interface pin (e.g. `"chat_completions"`), or None to
|
|
160
|
+
let the catalog choose among serveable interfaces.
|
|
161
|
+
tag: A revision-tag pin, or None for latest eligible.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
A composed `ChatModel` bound to the parsed model name.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If `spec` is not of the `"<provider>/<model>"` form.
|
|
168
|
+
ResolutionError: If the provider is not in the catalog (the message
|
|
169
|
+
names the providers that are), or no serveable interface
|
|
170
|
+
matches the pins.
|
|
171
|
+
LookupError: If a pinned interface has no registered component
|
|
172
|
+
factory (a declared stub, or a missing plugin distribution).
|
|
173
|
+
CompositionError: If the dialect declares no first-party endpoint
|
|
174
|
+
and no explicit `host` was given.
|
|
175
|
+
"""
|
|
176
|
+
# Deferred imports: `ocai.chat` sits below convert/runtime/wire in the
|
|
177
|
+
# layer map, so the front door reaches up only at call time, through the
|
|
178
|
+
# same documented seams as the facade (see [tool.importlinter] ignores).
|
|
179
|
+
from ocai.chat.facade import InterceptedChatModel
|
|
180
|
+
from ocai.chat.facade import ObservedChatModel
|
|
181
|
+
from ocai.convert.catalog import ResolutionError
|
|
182
|
+
from ocai.convert.catalog import default_catalog
|
|
183
|
+
from ocai.core.identity import CANONICAL_PROVIDER
|
|
184
|
+
from ocai.core.identity import CHAT
|
|
185
|
+
from ocai.runtime.binding import Binding
|
|
186
|
+
from ocai.runtime.binding import CompositionError
|
|
187
|
+
from ocai.wire.hosting import Custom
|
|
188
|
+
from ocai.wire.hosting import Direct
|
|
189
|
+
|
|
190
|
+
provider, slash, model_name = spec.partition("/")
|
|
191
|
+
if not slash or not provider or not model_name:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"chat model spec must be '<provider>/<model>' (split on the first "
|
|
194
|
+
f"slash; the model part may contain slashes), got {spec!r}"
|
|
195
|
+
)
|
|
196
|
+
composed = catalog if catalog is not None else default_catalog()
|
|
197
|
+
components = composed.components_for(provider, CHAT)
|
|
198
|
+
if not components:
|
|
199
|
+
known = sorted(
|
|
200
|
+
{
|
|
201
|
+
node.provider
|
|
202
|
+
for fragment in composed.fragments
|
|
203
|
+
for node in fragment.nodes
|
|
204
|
+
if node.provider != CANONICAL_PROVIDER
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
raise ResolutionError(
|
|
208
|
+
f"no chat component for provider {provider!r} (from spec {spec!r}) in "
|
|
209
|
+
f"this catalog; providers composed here: {', '.join(known) or 'none'}. "
|
|
210
|
+
f"If the plugin is not installed, install it with: pip install "
|
|
211
|
+
f"ocai-{provider}"
|
|
212
|
+
)
|
|
213
|
+
if interface is not None:
|
|
214
|
+
route = composed.resolve(CHAT, provider, interface=interface, tag=tag)
|
|
215
|
+
else:
|
|
216
|
+
# Unpinned: auto-select only among component-backed interfaces (the
|
|
217
|
+
# ones this catalog can serve). Multiple eligible sibling APIs require
|
|
218
|
+
# an explicit pin; newer dates do not imply interface preference.
|
|
219
|
+
routes: list[Route] = []
|
|
220
|
+
for component in components:
|
|
221
|
+
try:
|
|
222
|
+
routes.append(
|
|
223
|
+
composed.resolve(CHAT, provider, interface=component.interface, tag=tag)
|
|
224
|
+
)
|
|
225
|
+
except ResolutionError:
|
|
226
|
+
continue
|
|
227
|
+
if not routes:
|
|
228
|
+
pin = f" tag {tag!r}" if tag is not None else ""
|
|
229
|
+
raise ResolutionError(
|
|
230
|
+
f"no serveable chat interface for provider {provider!r}{pin} in this catalog"
|
|
231
|
+
)
|
|
232
|
+
if len(routes) > 1:
|
|
233
|
+
names = ", ".join(sorted(route.node.interface for route in routes))
|
|
234
|
+
raise ResolutionError(
|
|
235
|
+
f"ambiguous serveable interfaces for {provider}: {names}; "
|
|
236
|
+
"pass interface= explicitly"
|
|
237
|
+
)
|
|
238
|
+
route = routes[0]
|
|
239
|
+
codec = composed.component(provider, route.node.interface, CHAT)()
|
|
240
|
+
if isinstance(host, str):
|
|
241
|
+
host = Custom.from_descriptor(composed.host(host))
|
|
242
|
+
if host is None:
|
|
243
|
+
base_url = codec.dialect_traits().first_party_base_url
|
|
244
|
+
if base_url is None:
|
|
245
|
+
raise CompositionError(
|
|
246
|
+
f"{provider}/{route.node.interface} declares no first-party "
|
|
247
|
+
"endpoint (DialectTraits.first_party_base_url); pass host= "
|
|
248
|
+
"explicitly (Direct/Custom/Vertex, ocai.wire.hosting)"
|
|
249
|
+
)
|
|
250
|
+
host = Direct(base_url)
|
|
251
|
+
if transport is None:
|
|
252
|
+
from ocai.wire.transport import HttpxTransport
|
|
253
|
+
|
|
254
|
+
transport = HttpxTransport(provider=provider)
|
|
255
|
+
binding = Binding(host=host, credentials=credentials, transport=transport)
|
|
256
|
+
facade: WireChatModel | InterceptedChatModel | ObservedChatModel = WireChatModel(
|
|
257
|
+
codec,
|
|
258
|
+
binding,
|
|
259
|
+
composed,
|
|
260
|
+
model_name=model_name,
|
|
261
|
+
interface=interface,
|
|
262
|
+
tag=tag,
|
|
263
|
+
policies=policies,
|
|
264
|
+
attempt_observers=observers,
|
|
265
|
+
conversion_policy=conversion_policy,
|
|
266
|
+
)
|
|
267
|
+
if interceptors:
|
|
268
|
+
facade = InterceptedChatModel(facade, interceptors)
|
|
269
|
+
if observers:
|
|
270
|
+
facade = ObservedChatModel(facade, observers)
|
|
271
|
+
return facade
|
ocai/chat/canonical.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""The canonical chat graph nodes (architecture §2.1, §3).
|
|
2
|
+
|
|
3
|
+
The canonical IR is **just another interface** — provider `ocai`, interface
|
|
4
|
+
`ocai` — with dated revisions. These constants name the three canonical chat
|
|
5
|
+
nodes at the current revision; conversion edges to/from every provider
|
|
6
|
+
interface terminate on them, and IR self-migration (`ocai@2026-07 →
|
|
7
|
+
ocai@2027-01`) is ordinary edge traversal between two such node sets.
|
|
8
|
+
|
|
9
|
+
The chat IR dataclasses stamp these on `__node__`, and `WireChatModel` uses
|
|
10
|
+
them as the fixed end of every request/response/chunk conversion.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Final
|
|
16
|
+
|
|
17
|
+
from ocai.core.identity import CANONICAL_INTERFACE
|
|
18
|
+
from ocai.core.identity import CANONICAL_REVISION
|
|
19
|
+
from ocai.core.identity import CHAT_CHUNK
|
|
20
|
+
from ocai.core.identity import CHAT_REQUEST
|
|
21
|
+
from ocai.core.identity import CHAT_RESPONSE
|
|
22
|
+
from ocai.core.identity import NodeId
|
|
23
|
+
from ocai.core.identity import canonical_node
|
|
24
|
+
|
|
25
|
+
# The interface name of the canonical IR — canonical is not a special case:
|
|
26
|
+
# it is provider "ocai" publishing interface "ocai".
|
|
27
|
+
OCAI_INTERFACE: Final[str] = CANONICAL_INTERFACE
|
|
28
|
+
|
|
29
|
+
# The current canonical chat revision tag — a dated token on the `ocai`
|
|
30
|
+
# interface ("2026-07"), never the library semver, never a provider tag.
|
|
31
|
+
REVISION: Final[str] = CANONICAL_REVISION
|
|
32
|
+
|
|
33
|
+
# The canonical chat nodes at the current revision — the fixed endpoints of
|
|
34
|
+
# every chat conversion path.
|
|
35
|
+
CANONICAL_CHAT_REQUEST: Final[NodeId] = canonical_node(CHAT_REQUEST, REVISION)
|
|
36
|
+
CANONICAL_CHAT_RESPONSE: Final[NodeId] = canonical_node(CHAT_RESPONSE, REVISION)
|
|
37
|
+
CANONICAL_CHAT_CHUNK: Final[NodeId] = canonical_node(CHAT_CHUNK, REVISION)
|