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.
Files changed (79) hide show
  1. ocai/chat/__init__.py +271 -0
  2. ocai/chat/canonical.py +37 -0
  3. ocai/chat/facade.py +1572 -0
  4. ocai/chat/protocol.py +128 -0
  5. ocai/chat/py.typed +0 -0
  6. ocai/chat/types/__init__.py +195 -0
  7. ocai/chat/types/annotations.py +237 -0
  8. ocai/chat/types/config.py +336 -0
  9. ocai/chat/types/finish.py +36 -0
  10. ocai/chat/types/message.py +198 -0
  11. ocai/chat/types/parts.py +821 -0
  12. ocai/chat/types/request.py +124 -0
  13. ocai/chat/types/response.py +145 -0
  14. ocai/chat/types/role.py +90 -0
  15. ocai/chat/types/stream.py +1203 -0
  16. ocai/chat/types/tools.py +467 -0
  17. ocai/chat/types/usage.py +22 -0
  18. ocai/convert/__init__.py +89 -0
  19. ocai/convert/catalog.py +1029 -0
  20. ocai/convert/conformance.py +800 -0
  21. ocai/convert/contract.py +403 -0
  22. ocai/convert/graph.py +625 -0
  23. ocai/convert/hosts.py +70 -0
  24. ocai/convert/inspection.py +68 -0
  25. ocai/convert/migrate.py +69 -0
  26. ocai/convert/py.typed +0 -0
  27. ocai/convert/resources.py +148 -0
  28. ocai/core/__init__.py +212 -0
  29. ocai/core/_headers.py +45 -0
  30. ocai/core/canonjson.py +156 -0
  31. ocai/core/credentials.py +177 -0
  32. ocai/core/errors.py +190 -0
  33. ocai/core/fidelity.py +624 -0
  34. ocai/core/finish.py +396 -0
  35. ocai/core/identity.py +213 -0
  36. ocai/core/ir.py +328 -0
  37. ocai/core/jsonschema.py +326 -0
  38. ocai/core/primitives.py +114 -0
  39. ocai/core/profile.py +154 -0
  40. ocai/core/py.typed +0 -0
  41. ocai/core/refs.py +168 -0
  42. ocai/core/result.py +135 -0
  43. ocai/core/sources.py +251 -0
  44. ocai/core/traits.py +171 -0
  45. ocai/core/usage.py +246 -0
  46. ocai/embedding/__init__.py +28 -0
  47. ocai/embedding/protocol.py +77 -0
  48. ocai/embedding/py.typed +0 -0
  49. ocai/embedding/types/__init__.py +26 -0
  50. ocai/embedding/types/document.py +77 -0
  51. ocai/embedding/types/embedding.py +78 -0
  52. ocai/embedding/types/request.py +124 -0
  53. ocai/embedding/types/response.py +64 -0
  54. ocai/runtime/__init__.py +75 -0
  55. ocai/runtime/binding.py +120 -0
  56. ocai/runtime/deadline.py +58 -0
  57. ocai/runtime/decisions.py +217 -0
  58. ocai/runtime/drivers.py +772 -0
  59. ocai/runtime/dual.py +317 -0
  60. ocai/runtime/interceptors.py +307 -0
  61. ocai/runtime/observe.py +193 -0
  62. ocai/runtime/py.typed +0 -0
  63. ocai/vectorstore/__init__.py +26 -0
  64. ocai/vectorstore/protocol.py +255 -0
  65. ocai/vectorstore/py.typed +0 -0
  66. ocai/vectorstore/types/__init__.py +22 -0
  67. ocai/vectorstore/types/filter.py +14 -0
  68. ocai/vectorstore/types/record.py +106 -0
  69. ocai/wire/__init__.py +67 -0
  70. ocai/wire/framing.py +138 -0
  71. ocai/wire/hosting.py +497 -0
  72. ocai/wire/http.py +118 -0
  73. ocai/wire/policies.py +188 -0
  74. ocai/wire/py.typed +0 -0
  75. ocai/wire/sse.py +144 -0
  76. ocai/wire/transport.py +385 -0
  77. ocai-0.1.1.dist-info/METADATA +83 -0
  78. ocai-0.1.1.dist-info/RECORD +79 -0
  79. 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)