ocai 0.1.1__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.
Files changed (164) hide show
  1. ocai-0.1.1/.gitignore +60 -0
  2. ocai-0.1.1/CHANGELOG.md +37 -0
  3. ocai-0.1.1/PKG-INFO +83 -0
  4. ocai-0.1.1/README.md +56 -0
  5. ocai-0.1.1/pyproject.toml +51 -0
  6. ocai-0.1.1/src/ocai/chat/__init__.py +271 -0
  7. ocai-0.1.1/src/ocai/chat/canonical.py +37 -0
  8. ocai-0.1.1/src/ocai/chat/facade.py +1572 -0
  9. ocai-0.1.1/src/ocai/chat/protocol.py +128 -0
  10. ocai-0.1.1/src/ocai/chat/py.typed +0 -0
  11. ocai-0.1.1/src/ocai/chat/types/__init__.py +195 -0
  12. ocai-0.1.1/src/ocai/chat/types/annotations.py +237 -0
  13. ocai-0.1.1/src/ocai/chat/types/config.py +336 -0
  14. ocai-0.1.1/src/ocai/chat/types/finish.py +36 -0
  15. ocai-0.1.1/src/ocai/chat/types/message.py +198 -0
  16. ocai-0.1.1/src/ocai/chat/types/parts.py +821 -0
  17. ocai-0.1.1/src/ocai/chat/types/request.py +124 -0
  18. ocai-0.1.1/src/ocai/chat/types/response.py +145 -0
  19. ocai-0.1.1/src/ocai/chat/types/role.py +90 -0
  20. ocai-0.1.1/src/ocai/chat/types/stream.py +1203 -0
  21. ocai-0.1.1/src/ocai/chat/types/tools.py +467 -0
  22. ocai-0.1.1/src/ocai/chat/types/usage.py +22 -0
  23. ocai-0.1.1/src/ocai/convert/__init__.py +89 -0
  24. ocai-0.1.1/src/ocai/convert/catalog.py +1029 -0
  25. ocai-0.1.1/src/ocai/convert/conformance.py +800 -0
  26. ocai-0.1.1/src/ocai/convert/contract.py +403 -0
  27. ocai-0.1.1/src/ocai/convert/graph.py +625 -0
  28. ocai-0.1.1/src/ocai/convert/hosts.py +70 -0
  29. ocai-0.1.1/src/ocai/convert/inspection.py +68 -0
  30. ocai-0.1.1/src/ocai/convert/migrate.py +69 -0
  31. ocai-0.1.1/src/ocai/convert/py.typed +0 -0
  32. ocai-0.1.1/src/ocai/convert/resources.py +148 -0
  33. ocai-0.1.1/src/ocai/core/__init__.py +212 -0
  34. ocai-0.1.1/src/ocai/core/_headers.py +45 -0
  35. ocai-0.1.1/src/ocai/core/canonjson.py +156 -0
  36. ocai-0.1.1/src/ocai/core/credentials.py +177 -0
  37. ocai-0.1.1/src/ocai/core/errors.py +190 -0
  38. ocai-0.1.1/src/ocai/core/fidelity.py +624 -0
  39. ocai-0.1.1/src/ocai/core/finish.py +396 -0
  40. ocai-0.1.1/src/ocai/core/identity.py +213 -0
  41. ocai-0.1.1/src/ocai/core/ir.py +328 -0
  42. ocai-0.1.1/src/ocai/core/jsonschema.py +326 -0
  43. ocai-0.1.1/src/ocai/core/primitives.py +114 -0
  44. ocai-0.1.1/src/ocai/core/profile.py +154 -0
  45. ocai-0.1.1/src/ocai/core/py.typed +0 -0
  46. ocai-0.1.1/src/ocai/core/refs.py +168 -0
  47. ocai-0.1.1/src/ocai/core/result.py +135 -0
  48. ocai-0.1.1/src/ocai/core/sources.py +251 -0
  49. ocai-0.1.1/src/ocai/core/traits.py +171 -0
  50. ocai-0.1.1/src/ocai/core/usage.py +246 -0
  51. ocai-0.1.1/src/ocai/embedding/__init__.py +28 -0
  52. ocai-0.1.1/src/ocai/embedding/protocol.py +77 -0
  53. ocai-0.1.1/src/ocai/embedding/py.typed +0 -0
  54. ocai-0.1.1/src/ocai/embedding/types/__init__.py +26 -0
  55. ocai-0.1.1/src/ocai/embedding/types/document.py +77 -0
  56. ocai-0.1.1/src/ocai/embedding/types/embedding.py +78 -0
  57. ocai-0.1.1/src/ocai/embedding/types/request.py +124 -0
  58. ocai-0.1.1/src/ocai/embedding/types/response.py +64 -0
  59. ocai-0.1.1/src/ocai/runtime/__init__.py +75 -0
  60. ocai-0.1.1/src/ocai/runtime/binding.py +120 -0
  61. ocai-0.1.1/src/ocai/runtime/deadline.py +58 -0
  62. ocai-0.1.1/src/ocai/runtime/decisions.py +217 -0
  63. ocai-0.1.1/src/ocai/runtime/drivers.py +772 -0
  64. ocai-0.1.1/src/ocai/runtime/dual.py +317 -0
  65. ocai-0.1.1/src/ocai/runtime/interceptors.py +307 -0
  66. ocai-0.1.1/src/ocai/runtime/observe.py +193 -0
  67. ocai-0.1.1/src/ocai/runtime/py.typed +0 -0
  68. ocai-0.1.1/src/ocai/vectorstore/__init__.py +26 -0
  69. ocai-0.1.1/src/ocai/vectorstore/protocol.py +255 -0
  70. ocai-0.1.1/src/ocai/vectorstore/py.typed +0 -0
  71. ocai-0.1.1/src/ocai/vectorstore/types/__init__.py +22 -0
  72. ocai-0.1.1/src/ocai/vectorstore/types/filter.py +14 -0
  73. ocai-0.1.1/src/ocai/vectorstore/types/record.py +106 -0
  74. ocai-0.1.1/src/ocai/wire/__init__.py +67 -0
  75. ocai-0.1.1/src/ocai/wire/framing.py +138 -0
  76. ocai-0.1.1/src/ocai/wire/hosting.py +497 -0
  77. ocai-0.1.1/src/ocai/wire/http.py +118 -0
  78. ocai-0.1.1/src/ocai/wire/policies.py +188 -0
  79. ocai-0.1.1/src/ocai/wire/py.typed +0 -0
  80. ocai-0.1.1/src/ocai/wire/sse.py +144 -0
  81. ocai-0.1.1/src/ocai/wire/transport.py +385 -0
  82. ocai-0.1.1/tests/conftest.py +76 -0
  83. ocai-0.1.1/tests/http/anthropic_messages.json +30 -0
  84. ocai-0.1.1/tests/http/anthropic_messages_auth_error.json +28 -0
  85. ocai-0.1.1/tests/http/anthropic_messages_overloaded_error.json +28 -0
  86. ocai-0.1.1/tests/http/anthropic_messages_stream.json +35 -0
  87. ocai-0.1.1/tests/http/anthropic_messages_stream_error.json +31 -0
  88. ocai-0.1.1/tests/http/anthropic_messages_tool_call.json +57 -0
  89. ocai-0.1.1/tests/http/anthropic_server_tool_unsupported.json +52 -0
  90. ocai-0.1.1/tests/http/anthropic_server_tool_unsupported_stream.json +49 -0
  91. ocai-0.1.1/tests/http/anthropic_thinking_replay.json +68 -0
  92. ocai-0.1.1/tests/http/anthropic_thinking_replay_stream.json +65 -0
  93. ocai-0.1.1/tests/http/anthropic_thinking_seed.json +50 -0
  94. ocai-0.1.1/tests/http/anthropic_vertex_version.json +43 -0
  95. ocai-0.1.1/tests/http/anthropic_web_search_error.json +76 -0
  96. ocai-0.1.1/tests/http/anthropic_web_search_error_stream.json +57 -0
  97. ocai-0.1.1/tests/http/anthropic_web_search_error_stream_paired.json +76 -0
  98. ocai-0.1.1/tests/http/anthropic_web_search_interrupted.json +49 -0
  99. ocai-0.1.1/tests/http/anthropic_web_search_replay.json +150 -0
  100. ocai-0.1.1/tests/http/anthropic_web_search_stream.json +58 -0
  101. ocai-0.1.1/tests/http/anthropic_web_search_stream_paired.json +96 -0
  102. ocai-0.1.1/tests/http/custom_host_arn.json +53 -0
  103. ocai-0.1.1/tests/http/custom_host_stream.json +34 -0
  104. ocai-0.1.1/tests/http/custom_host_unary.json +53 -0
  105. ocai-0.1.1/tests/http/gateway_anthropic_tools.json +75 -0
  106. ocai-0.1.1/tests/http/gateway_anthropic_tools_stream.json +63 -0
  107. ocai-0.1.1/tests/http/gateway_openai_tools.json +67 -0
  108. ocai-0.1.1/tests/http/gateway_openai_tools_stream.json +55 -0
  109. ocai-0.1.1/tests/http/openai_chat_citation_unqualified.json +53 -0
  110. ocai-0.1.1/tests/http/openai_chat_completion.json +31 -0
  111. ocai-0.1.1/tests/http/openai_chat_completion_auth_error.json +24 -0
  112. ocai-0.1.1/tests/http/openai_chat_completion_context_length.json +24 -0
  113. ocai-0.1.1/tests/http/openai_chat_completion_reasoning.json +31 -0
  114. ocai-0.1.1/tests/http/openai_chat_completion_retry.json +45 -0
  115. ocai-0.1.1/tests/http/openai_chat_completion_stream.json +34 -0
  116. ocai-0.1.1/tests/http/openai_chat_completion_stream_abort.json +31 -0
  117. ocai-0.1.1/tests/http/openai_chat_completion_stream_extra_choice.json +34 -0
  118. ocai-0.1.1/tests/http/openai_chat_completion_stream_paired.json +53 -0
  119. ocai-0.1.1/tests/http/openai_chat_completion_tool_call.json +73 -0
  120. ocai-0.1.1/tests/http/openai_chat_framed_stream.json +34 -0
  121. ocai-0.1.1/tests/http/openai_chat_framed_stream_paired.json +53 -0
  122. ocai-0.1.1/tests/http/openai_server_tool_unsupported.json +64 -0
  123. ocai-0.1.1/tests/http/openai_server_tool_unsupported_stream.json +45 -0
  124. ocai-0.1.1/tests/test_anthropic_search.py +308 -0
  125. ocai-0.1.1/tests/test_catalog_hardening.py +271 -0
  126. ocai-0.1.1/tests/test_citations.py +176 -0
  127. ocai-0.1.1/tests/test_conformance.py +202 -0
  128. ocai-0.1.1/tests/test_conformance_resources.py +219 -0
  129. ocai-0.1.1/tests/test_context_deadline.py +243 -0
  130. ocai-0.1.1/tests/test_contract_certify.py +302 -0
  131. ocai-0.1.1/tests/test_credential_isolation.py +174 -0
  132. ocai-0.1.1/tests/test_credentials.py +60 -0
  133. ocai-0.1.1/tests/test_cross_conversion.py +363 -0
  134. ocai-0.1.1/tests/test_errors.py +73 -0
  135. ocai-0.1.1/tests/test_facade_stream.py +339 -0
  136. ocai-0.1.1/tests/test_facade_stream_anthropic.py +389 -0
  137. ocai-0.1.1/tests/test_facade_unary.py +453 -0
  138. ocai-0.1.1/tests/test_facade_unary_anthropic.py +490 -0
  139. ocai-0.1.1/tests/test_fidelity_policy.py +148 -0
  140. ocai-0.1.1/tests/test_fold_law.py +425 -0
  141. ocai-0.1.1/tests/test_front_doors.py +376 -0
  142. ocai-0.1.1/tests/test_gateway_qualification.py +416 -0
  143. ocai-0.1.1/tests/test_host_composition.py +378 -0
  144. ocai-0.1.1/tests/test_host_traits.py +303 -0
  145. ocai-0.1.1/tests/test_import_contracts.py +86 -0
  146. ocai-0.1.1/tests/test_item_proposal.py +131 -0
  147. ocai-0.1.1/tests/test_lifecycle_events.py +268 -0
  148. ocai-0.1.1/tests/test_namespace.py +59 -0
  149. ocai-0.1.1/tests/test_no_sdk.py +51 -0
  150. ocai-0.1.1/tests/test_profile_gate.py +348 -0
  151. ocai-0.1.1/tests/test_public_preflight.py +233 -0
  152. ocai-0.1.1/tests/test_replay_anchors.py +204 -0
  153. ocai-0.1.1/tests/test_schema_conformance.py +551 -0
  154. ocai-0.1.1/tests/test_server_tool_intent.py +203 -0
  155. ocai-0.1.1/tests/test_skeleton.py +161 -0
  156. ocai-0.1.1/tests/test_spec_schemas.py +370 -0
  157. ocai-0.1.1/tests/test_stream_framing.py +660 -0
  158. ocai-0.1.1/tests/test_stream_middleware.py +236 -0
  159. ocai-0.1.1/tests/test_stream_ownership.py +185 -0
  160. ocai-0.1.1/tests/test_stream_planning.py +466 -0
  161. ocai-0.1.1/tests/test_streaming.py +169 -0
  162. ocai-0.1.1/tests/test_transport.py +103 -0
  163. ocai-0.1.1/tests/test_unknown_inputs.py +188 -0
  164. ocai-0.1.1/tests/test_version_fidelity.py +175 -0
ocai-0.1.1/.gitignore ADDED
@@ -0,0 +1,60 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ *.so
9
+
10
+ # uv / venv
11
+ .venv/
12
+ uv.lock.bak
13
+
14
+ # Tooling caches
15
+ .pytest_cache/
16
+ .ruff_cache/
17
+ .mypy_cache/
18
+ .import_linter_cache/
19
+ .coverage
20
+ htmlcov/
21
+
22
+ # Docs site build output and page cache (zensical build)
23
+ /site/
24
+ /.cache/
25
+
26
+ # Node (future typescript/)
27
+ node_modules/
28
+ *.tsbuildinfo
29
+
30
+ # Go (future go/)
31
+ /go/bin/
32
+
33
+ # Editors / OS. Shared editor settings ARE committed: the multi-root workspace
34
+ # (ocai.code-workspace) and each folder's .vscode/settings.json + extensions.json
35
+ # give Pylance/Ruff the right interpreter per project. Personal files stay local.
36
+ .idea/
37
+ .vscode/*
38
+ !.vscode/settings.json
39
+ !.vscode/extensions.json
40
+ .DS_Store
41
+
42
+ # Per-user agent settings (never commit personal permissions)
43
+ .claude/settings.local.json
44
+ .codex/config.toml
45
+
46
+ # Platform build and browser-test artifacts
47
+ .nuxt/
48
+ .output/
49
+ platform/test-results/
50
+ platform/playwright-report/
51
+
52
+ # Local environment files: the root .env is the active one, .envs/ holds the
53
+ # per-environment variants (`task env -- <ENV>` copies .envs/.env.<ENV> to .env).
54
+ # Never committed: they hold credentials.
55
+ /.env
56
+ /.envs/
57
+ .local/
58
+
59
+ # Task execution fingerprints
60
+ .task/
@@ -0,0 +1,37 @@
1
+ # Changelog — ocai
2
+
3
+ All notable changes to the `ocai` distribution. Generated from the change files in
4
+ `.changeset/` by the release workflow (ADR 0010); do not edit released sections by hand.
5
+ ## 0.1.1 (2026-11-11)
6
+
7
+ ### Features
8
+
9
+ #### Compose explicit host endpoints and profile aliases
10
+
11
+ Compose explicit host endpoints and profile aliases. (WU-38, #35)
12
+
13
+ #### Select stream framing independently of semantic conversion
14
+
15
+ Select stream framing independently of semantic conversion. (WU-39, #69)
16
+
17
+ #### Validate and preserve canonical URL citations
18
+
19
+ Validate and preserve canonical URL citations. (WU-40, #77)
20
+
21
+ #### Admit provider-executed server-tool definitions with honest support reports
22
+
23
+ Admit provider-executed server-tool definitions with honest support reports. (WU-41, #80)
24
+
25
+ #### Preserve scoped request and replay anchors
26
+
27
+ Preserve scoped request and replay anchors. (WU-42, #83)
28
+
29
+ #### Qualify basic Anthropic web search with replay and measured accounting
30
+
31
+ Qualify basic Anthropic web search with replay and measured accounting. (WU-43, #87)
32
+
33
+ ### Fixes
34
+
35
+ #### Package metadata points at the real repository and the DeepSeek plugin has an extra
36
+
37
+ Homepage, Source, Documentation and Changelog URLs now point at `github.com/ocarinow/ocai` and the docs site. `ocai[deepseek]` installs the DeepSeek plugin and `ocai[all]` includes it. No runtime behavior changed.
ocai-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: ocai
3
+ Version: 0.1.1
4
+ Summary: One Common interface for AI: ports & adapters + middleware + a canonical-IR conversion graph. Source-available.
5
+ Project-URL: Homepage, https://github.com/ocarinow/ocai
6
+ Project-URL: Source, https://github.com/ocarinow/ocai
7
+ Project-URL: Documentation, https://ocarinow.github.io/ocai/
8
+ Project-URL: Changelog, https://github.com/ocarinow/ocai/blob/main/python/packages/ocai-core/CHANGELOG.md
9
+ Author: ocai maintainers
10
+ License-Expression: FSL-1.1-ALv2
11
+ Keywords: adapters,ai,interoperability,llm,providers,rag
12
+ Requires-Python: >=3.13
13
+ Provides-Extra: all
14
+ Requires-Dist: httpx>=0.27; extra == 'all'
15
+ Requires-Dist: ocai-anthropic; extra == 'all'
16
+ Requires-Dist: ocai-deepseek; extra == 'all'
17
+ Requires-Dist: ocai-openai; extra == 'all'
18
+ Provides-Extra: anthropic
19
+ Requires-Dist: ocai-anthropic; extra == 'anthropic'
20
+ Provides-Extra: deepseek
21
+ Requires-Dist: ocai-deepseek; extra == 'deepseek'
22
+ Provides-Extra: http
23
+ Requires-Dist: httpx>=0.27; extra == 'http'
24
+ Provides-Extra: openai
25
+ Requires-Dist: ocai-openai; extra == 'openai'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # `ocai` (core)
29
+
30
+ The core distribution of [ocai](https://github.com/ocarinow/ocai), One Common
31
+ interface for AI. Distribution name: **`ocai`**. Requires Python 3.13+.
32
+
33
+ It ships the shared layers into the `ocai` PEP 420 namespace:
34
+
35
+ - `ocai.core` — pure-stdlib data contracts: identity, fidelity, errors,
36
+ `CallResult`, credentials, provider references, the IR envelope, canonical
37
+ JSON.
38
+ - `ocai.chat`, `ocai.embedding`, `ocai.vectorstore` — capability ports and
39
+ canonical IR types; chat is the one exercised end to end today, the other
40
+ two are provisional contracts.
41
+ - `ocai.convert` — governed conversion catalogs, graph planning, edge
42
+ contracts, the conformance harness.
43
+ - `ocai.runtime` — sans-io drivers, bindings, policy decisions, interceptors.
44
+ - `ocai.wire` — HTTP/SSE transport, hosting, wire policies.
45
+
46
+ There is deliberately **no** `ocai/__init__.py` in this distribution, so
47
+ provider distributions (`ocai-openai`, `ocai-anthropic`, community
48
+ `ocai-contrib-*` packages) can add their own subpackages to the same
49
+ namespace. Importing `ocai.core` or `ocai.convert` needs no third-party
50
+ package at all.
51
+
52
+ ## Extras
53
+
54
+ - `ocai[http]` — `httpx`, used only by `ocai.wire.transport.HttpxTransport`
55
+ and imported lazily there. Required for live calls.
56
+ - `ocai[openai]`, `ocai[anthropic]` — the sibling plugin distributions
57
+ (`ocai-openai`, `ocai-anthropic`); the extra and the standalone package
58
+ resolve to the same wheel.
59
+ - `ocai[all]` — all of the above.
60
+
61
+ ## Usage
62
+
63
+ ```python
64
+ import ocai.chat
65
+ from ocai.chat import ChatRequest
66
+ from ocai.chat import Message
67
+ from ocai.core import ApiKey
68
+
69
+ model = ocai.chat.model("openai/gpt-4o", credentials=ApiKey.from_env("OPENAI_API_KEY"))
70
+ result = model.chat(ChatRequest(messages=[Message.user("Hello!")]))
71
+ print(result.value.message.text, result.report.lossless)
72
+ ```
73
+
74
+ A live call needs `ocai[http]`, an installed provider plugin, and a real key;
75
+ credentials are explicit and never discovered from the environment unless
76
+ `ApiKey.from_env` is called. Every port returns
77
+ `CallResult(value, route, report, usage, raw)`. The full guide is the
78
+ [Python README](https://github.com/ocarinow/ocai/blob/main/python/README.md).
79
+
80
+ ## License
81
+
82
+ Source-available under FSL-1.1-ALv2 (converting to Apache-2.0 two years after
83
+ each release). ocai is not open source.
ocai-0.1.1/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # `ocai` (core)
2
+
3
+ The core distribution of [ocai](https://github.com/ocarinow/ocai), One Common
4
+ interface for AI. Distribution name: **`ocai`**. Requires Python 3.13+.
5
+
6
+ It ships the shared layers into the `ocai` PEP 420 namespace:
7
+
8
+ - `ocai.core` — pure-stdlib data contracts: identity, fidelity, errors,
9
+ `CallResult`, credentials, provider references, the IR envelope, canonical
10
+ JSON.
11
+ - `ocai.chat`, `ocai.embedding`, `ocai.vectorstore` — capability ports and
12
+ canonical IR types; chat is the one exercised end to end today, the other
13
+ two are provisional contracts.
14
+ - `ocai.convert` — governed conversion catalogs, graph planning, edge
15
+ contracts, the conformance harness.
16
+ - `ocai.runtime` — sans-io drivers, bindings, policy decisions, interceptors.
17
+ - `ocai.wire` — HTTP/SSE transport, hosting, wire policies.
18
+
19
+ There is deliberately **no** `ocai/__init__.py` in this distribution, so
20
+ provider distributions (`ocai-openai`, `ocai-anthropic`, community
21
+ `ocai-contrib-*` packages) can add their own subpackages to the same
22
+ namespace. Importing `ocai.core` or `ocai.convert` needs no third-party
23
+ package at all.
24
+
25
+ ## Extras
26
+
27
+ - `ocai[http]` — `httpx`, used only by `ocai.wire.transport.HttpxTransport`
28
+ and imported lazily there. Required for live calls.
29
+ - `ocai[openai]`, `ocai[anthropic]` — the sibling plugin distributions
30
+ (`ocai-openai`, `ocai-anthropic`); the extra and the standalone package
31
+ resolve to the same wheel.
32
+ - `ocai[all]` — all of the above.
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ import ocai.chat
38
+ from ocai.chat import ChatRequest
39
+ from ocai.chat import Message
40
+ from ocai.core import ApiKey
41
+
42
+ model = ocai.chat.model("openai/gpt-4o", credentials=ApiKey.from_env("OPENAI_API_KEY"))
43
+ result = model.chat(ChatRequest(messages=[Message.user("Hello!")]))
44
+ print(result.value.message.text, result.report.lossless)
45
+ ```
46
+
47
+ A live call needs `ocai[http]`, an installed provider plugin, and a real key;
48
+ credentials are explicit and never discovered from the environment unless
49
+ `ApiKey.from_env` is called. Every port returns
50
+ `CallResult(value, route, report, usage, raw)`. The full guide is the
51
+ [Python README](https://github.com/ocarinow/ocai/blob/main/python/README.md).
52
+
53
+ ## License
54
+
55
+ Source-available under FSL-1.1-ALv2 (converting to Apache-2.0 two years after
56
+ each release). ocai is not open source.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ # Distribution name is "ocai" — the meta-package / core. Plugin distributions
7
+ # ship additional subpackages into the SAME `ocai` namespace.
8
+ name = "ocai"
9
+ version = "0.1.1" # library public API version (semver), kept stable
10
+ description = "One Common interface for AI: ports & adapters + middleware + a canonical-IR conversion graph. Source-available."
11
+ readme = "README.md"
12
+ requires-python = ">=3.13"
13
+ license = "FSL-1.1-ALv2" # source-available (FSL → Apache-2.0 after 2 years)
14
+ authors = [{ name = "ocai maintainers" }]
15
+ keywords = ["llm", "ai", "providers", "adapters", "rag", "interoperability"]
16
+ # Zero hard runtime deps: the contract layers are pure stdlib (rule 11), and
17
+ # even the wire stack imports httpx lazily inside HttpxTransport, behind the
18
+ # [http] extra — importing ocai.core / ocai.convert never requires httpx.
19
+ dependencies = []
20
+
21
+ [project.optional-dependencies]
22
+ # The HTTP leaf: ocai owns the wire (no provider SDKs, rule 12); httpx is used
23
+ # solely by ocai.wire.transport.HttpxTransport and imported lazily there.
24
+ http = ["httpx>=0.27"]
25
+
26
+ # Extras pull in sibling PLUGIN distributions (Airflow pattern). Each plugin
27
+ # adapts a provider via the shared wire stack — never a provider SDK (rule 12).
28
+ # `pip install ocai[openai]` and `pip install ocai-openai` resolve to the same wheel.
29
+ openai = ["ocai-openai"]
30
+ anthropic = ["ocai-anthropic"]
31
+ deepseek = ["ocai-deepseek"]
32
+
33
+ # Convenience aggregate.
34
+ all = ["ocai[http,openai,anthropic,deepseek]"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/ocarinow/ocai"
38
+ Source = "https://github.com/ocarinow/ocai"
39
+ Documentation = "https://ocarinow.github.io/ocai/"
40
+ Changelog = "https://github.com/ocarinow/ocai/blob/main/python/packages/ocai-core/CHANGELOG.md"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ # Map src/ocai -> the `ocai` namespace in the wheel. There is intentionally NO
44
+ # src/ocai/__init__.py (PEP 420), so sibling distributions can contribute their
45
+ # own subpackages into the same namespace.
46
+ packages = ["src/ocai"]
47
+
48
+ # Add the src dir to sys.path on editable installs so the PEP 420 `ocai`
49
+ # namespace is discovered and merged across multiple editable distributions.
50
+ [tool.hatch.build]
51
+ dev-mode-dirs = ["src"]
@@ -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
@@ -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)