python-corekit 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. {python_corekit-0.3.0/python_corekit.egg-info → python_corekit-0.4.0}/PKG-INFO +33 -4
  2. {python_corekit-0.3.0 → python_corekit-0.4.0}/README.md +30 -2
  3. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/__init__.py +5 -0
  4. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/client.py +34 -3
  5. python_corekit-0.4.0/corekit/http/stream.py +110 -0
  6. python_corekit-0.4.0/corekit/llm/__init__.py +134 -0
  7. python_corekit-0.4.0/corekit/llm/client.py +179 -0
  8. python_corekit-0.4.0/corekit/llm/enum.py +123 -0
  9. python_corekit-0.4.0/corekit/llm/events.py +96 -0
  10. python_corekit-0.4.0/corekit/llm/messages.py +173 -0
  11. python_corekit-0.4.0/corekit/llm/prompts/__init__.py +19 -0
  12. python_corekit-0.4.0/corekit/llm/prompts/enum.py +54 -0
  13. python_corekit-0.4.0/corekit/llm/prompts/exceptions.py +22 -0
  14. python_corekit-0.4.0/corekit/llm/prompts/loader.py +139 -0
  15. python_corekit-0.4.0/corekit/llm/prompts/template.py +53 -0
  16. python_corekit-0.4.0/corekit/llm/protocols.py +65 -0
  17. python_corekit-0.4.0/corekit/llm/streaming.py +149 -0
  18. python_corekit-0.4.0/corekit/llm/tools/__init__.py +19 -0
  19. python_corekit-0.4.0/corekit/llm/tools/base.py +118 -0
  20. python_corekit-0.4.0/corekit/llm/tools/detection.py +99 -0
  21. python_corekit-0.4.0/corekit/llm/tools/loop.py +255 -0
  22. python_corekit-0.4.0/corekit/llm/tools/registry.py +103 -0
  23. python_corekit-0.4.0/corekit/llm/wire.py +199 -0
  24. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/__init__.py +3 -3
  25. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/benchmarkable.py +16 -2
  26. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/timing/split.py +14 -0
  27. python_corekit-0.4.0/corekit/observability/timing/timer.py +54 -0
  28. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/__init__.py +2 -1
  29. python_corekit-0.4.0/corekit/schemas/version.py +58 -0
  30. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/__init__.py +2 -1
  31. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/collections.py +16 -1
  32. {python_corekit-0.3.0 → python_corekit-0.4.0}/pyproject.toml +5 -2
  33. {python_corekit-0.3.0 → python_corekit-0.4.0/python_corekit.egg-info}/PKG-INFO +33 -4
  34. {python_corekit-0.3.0 → python_corekit-0.4.0}/python_corekit.egg-info/SOURCES.txt +21 -0
  35. {python_corekit-0.3.0 → python_corekit-0.4.0}/python_corekit.egg-info/requires.txt +1 -0
  36. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_architecture.py +1 -0
  37. python_corekit-0.4.0/tests/test_llm.py +492 -0
  38. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_observability.py +24 -0
  39. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_requests.py +133 -1
  40. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_schemas_enum.py +12 -1
  41. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils.py +15 -0
  42. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_collections.py +11 -0
  43. python_corekit-0.3.0/corekit/observability/timing/timer.py +0 -32
  44. {python_corekit-0.3.0 → python_corekit-0.4.0}/LICENSE +0 -0
  45. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/__init__.py +0 -0
  46. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/__init__.py +0 -0
  47. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/application.py +0 -0
  48. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/handler.py +0 -0
  49. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/lifespan.py +0 -0
  50. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/middleware.py +0 -0
  51. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/responses.py +0 -0
  52. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/routers.py +0 -0
  53. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/concurrency/__init__.py +0 -0
  54. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/concurrency/decorators.py +0 -0
  55. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/concurrency/thread_local.py +0 -0
  56. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/concurrency/worker.py +0 -0
  57. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/config/__init__.py +0 -0
  58. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/config/loader.py +0 -0
  59. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/config/settings.py +0 -0
  60. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/config/sources.py +0 -0
  61. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/__init__.py +0 -0
  62. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/connectable.py +0 -0
  63. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/decorators.py +0 -0
  64. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/redis/__init__.py +0 -0
  65. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/redis/connection.py +0 -0
  66. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/registry.py +0 -0
  67. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/__init__.py +0 -0
  68. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/connection.py +0 -0
  69. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/fields/__init__.py +0 -0
  70. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/fields/jsonb.py +0 -0
  71. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/__init__.py +0 -0
  72. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/base.py +0 -0
  73. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/operations.py +0 -0
  74. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/registry.py +0 -0
  75. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/table.py +0 -0
  76. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/operations/__init__.py +0 -0
  77. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/operations/base.py +0 -0
  78. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/operations/statements.py +0 -0
  79. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/query.py +0 -0
  80. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/table.py +0 -0
  81. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/crypto/__init__.py +0 -0
  82. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/crypto/constants.py +0 -0
  83. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/crypto/enum.py +0 -0
  84. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/crypto/hasher.py +0 -0
  85. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/__init__.py +0 -0
  86. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/dataset.py +0 -0
  87. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/__init__.py +0 -0
  88. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/comparison.py +0 -0
  89. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/expression.py +0 -0
  90. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/operator.py +0 -0
  91. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/target.py +0 -0
  92. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/record.py +0 -0
  93. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/stats.py +0 -0
  94. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/decorators/__init__.py +0 -0
  95. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/decorators/exception_handling.py +0 -0
  96. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/decorators/warnings.py +0 -0
  97. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/docker/__init__.py +0 -0
  98. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/docker/watchdog.py +0 -0
  99. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/__init__.py +0 -0
  100. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/connection.py +0 -0
  101. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/extract/__init__.py +0 -0
  102. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/extract/extractor.py +0 -0
  103. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/extract/schemas.py +0 -0
  104. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/load/__init__.py +0 -0
  105. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/load/loader.py +0 -0
  106. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/load/schemas.py +0 -0
  107. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/orchestrator.py +0 -0
  108. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/schemas.py +0 -0
  109. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/transform/__init__.py +0 -0
  110. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/transform/schemas.py +0 -0
  111. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/transform/transformer.py +0 -0
  112. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/__init__.py +0 -0
  113. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/enum.py +0 -0
  114. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/frames.py +0 -0
  115. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/models.py +0 -0
  116. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/publisher.py +0 -0
  117. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/reader.py +0 -0
  118. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/sse.py +0 -0
  119. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/websocket.py +0 -0
  120. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/__init__.py +0 -0
  121. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/base.py +0 -0
  122. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/custom/__init__.py +0 -0
  123. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/enum.py +0 -0
  124. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/http/__init__.py +0 -0
  125. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/types.py +0 -0
  126. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/__init__.py +0 -0
  127. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/base.py +0 -0
  128. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/enum.py +0 -0
  129. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/json.py +0 -0
  130. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/toml.py +0 -0
  131. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/api.py +0 -0
  132. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/exceptions.py +0 -0
  133. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/exponential_backoff.py +0 -0
  134. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/response.py +0 -0
  135. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/status.py +0 -0
  136. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/jobs/__init__.py +0 -0
  137. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/jobs/registry.py +0 -0
  138. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/jobs/runner.py +0 -0
  139. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/jobs/task.py +0 -0
  140. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/log_monitor/__init__.py +0 -0
  141. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/log_monitor/constants.py +0 -0
  142. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/log_monitor/models.py +0 -0
  143. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/log_monitor/service.py +0 -0
  144. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/notifications/__init__.py +0 -0
  145. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/notifications/base.py +0 -0
  146. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/notifications/models.py +0 -0
  147. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/loggable.py +0 -0
  148. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/request_context.py +0 -0
  149. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/timing/__init__.py +0 -0
  150. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/timing/constants.py +0 -0
  151. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/py.typed +0 -0
  152. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/registry/__init__.py +0 -0
  153. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/registry/ordered.py +0 -0
  154. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/registry/registry.py +0 -0
  155. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/dataclasses/__init__.py +0 -0
  156. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/enum.py +0 -0
  157. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/models/__init__.py +0 -0
  158. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/models/arbitrary.py +0 -0
  159. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/models/date_models.py +0 -0
  160. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/pydantic/__init__.py +0 -0
  161. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/pydantic/fields.py +0 -0
  162. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/types.py +0 -0
  163. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/__init__.py +0 -0
  164. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/enum.py +0 -0
  165. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/pickle_file.py +0 -0
  166. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/serializable.py +0 -0
  167. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/serializer.py +0 -0
  168. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/coercion.py +0 -0
  169. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/ids.py +0 -0
  170. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/payload.py +0 -0
  171. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/raise_exc.py +0 -0
  172. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/text.py +0 -0
  173. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/time.py +0 -0
  174. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/validators.py +0 -0
  175. {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/void.py +0 -0
  176. {python_corekit-0.3.0 → python_corekit-0.4.0}/python_corekit.egg-info/dependency_links.txt +0 -0
  177. {python_corekit-0.3.0 → python_corekit-0.4.0}/python_corekit.egg-info/top_level.txt +0 -0
  178. {python_corekit-0.3.0 → python_corekit-0.4.0}/setup.cfg +0 -0
  179. {python_corekit-0.3.0 → python_corekit-0.4.0}/setup.py +0 -0
  180. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_api.py +0 -0
  181. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_application.py +0 -0
  182. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_concurrency.py +0 -0
  183. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_config.py +0 -0
  184. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_connections.py +0 -0
  185. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_data.py +0 -0
  186. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_docker.py +0 -0
  187. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_etl.py +0 -0
  188. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_events.py +0 -0
  189. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_exceptions.py +0 -0
  190. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_expressions.py +0 -0
  191. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_expressions_sqlalchemy.py +0 -0
  192. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_files.py +0 -0
  193. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_imports.py +0 -0
  194. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_imports_are_top_level.py +0 -0
  195. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_jobs.py +0 -0
  196. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_lifespan.py +0 -0
  197. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_log_monitor.py +0 -0
  198. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_middleware_stack.py +0 -0
  199. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_migration.py +0 -0
  200. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_notifications.py +0 -0
  201. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_operations.py +0 -0
  202. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_ordered_registry.py +0 -0
  203. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_redis.py +0 -0
  204. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_registry.py +0 -0
  205. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_request_context.py +0 -0
  206. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_serialization.py +0 -0
  207. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_sql.py +0 -0
  208. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_coercion.py +0 -0
  209. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_ids.py +0 -0
  210. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_payload.py +0 -0
  211. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_text.py +0 -0
  212. {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_time.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-corekit
3
- Version: 0.3.0
4
- Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI application and routers, SQL statements and migrations, background tasks, and ETL
3
+ Version: 0.4.0
4
+ Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI application and routers, SQL statements and migrations, background tasks, ETL, and LLM chat/tool primitives
5
5
  Author: Steven Jacobsen
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/stevejaker/corekit
@@ -18,6 +18,7 @@ License-File: LICENSE
18
18
  Requires-Dist: pydantic<3,>=2.10
19
19
  Requires-Dist: pydantic-settings<3,>=2.0
20
20
  Requires-Dist: fastapi<1,>=0.115
21
+ Requires-Dist: starlette<0.47,>=0.40
21
22
  Requires-Dist: sqlmodel<0.1,>=0.0.16
22
23
  Requires-Dist: SQLAlchemy<3,>=2.0
23
24
  Requires-Dist: redis<7,>=5.0
@@ -37,7 +38,8 @@ Dynamic: license-file
37
38
 
38
39
  Shared foundations for Python projects: structured logging, benchmarking,
39
40
  registries, a FastAPI application with routers and handlers, SQL statements and
40
- migrations, background tasks, an in-memory record store, and ETL scaffolding.
41
+ migrations, background tasks, an in-memory record store, ETL scaffolding, and
42
+ OpenAI-shaped chat / tool-loop primitives.
41
43
 
42
44
  Requires Python 3.11+.
43
45
 
@@ -55,7 +57,7 @@ extras to remember, and no import that fails because something was left out.
55
57
  Pin a compatible release rather than tracking whatever is newest:
56
58
 
57
59
  ```
58
- python-corekit~=0.3.0
60
+ python-corekit~=0.4.0
59
61
  ```
60
62
 
61
63
  Before 1.0, the minor version carries breaking changes.
@@ -281,6 +283,32 @@ ceiling, not 9,999 threads.
281
283
  Failures propagate by default. Pass `raise_on_error=False` to log and skip them
282
284
  instead, which loses results silently and so is opt-in.
283
285
 
286
+ ## LLM chat and tools
287
+
288
+ OpenAI-shaped messages, a tool registry, an agentic loop, and a thin
289
+ ``ChatCompletionsClient`` (a ``BaseApiClient``) for any OpenAI-compatible
290
+ server — llama.cpp, vLLM, OpenAI, and so on. Not a multi-provider generation
291
+ facade.
292
+
293
+ ```python
294
+ from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
295
+
296
+ client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
297
+ prompts = PromptLoader("/path/to/prompts") # mount any compatible tree
298
+ tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
299
+
300
+ registry = ToolRegistry()
301
+ registry.register(MyTool())
302
+ loop = ToolLoop(client, registry)
303
+ async for event in loop.stream(tmpl.messages(text="…", style="brief", length="short")):
304
+ ...
305
+ ```
306
+
307
+ ``PromptLoader`` reads versioned ``.txt`` files from a directory you point at
308
+ (``{kind}/{name}/{role}-v{version}.txt``, plus ``common/`` fragments). Prompt
309
+ *text* stays with the consumer — corekit only ships the loader. See
310
+ `docs/LLM_EXTRACTION.md`.
311
+
284
312
  ## HTTP clients
285
313
 
286
314
  ```python
@@ -394,6 +422,7 @@ corekit/
394
422
 
395
423
  api/ Application, lifespan, middleware, routers
396
424
  docker/ notifications/ etl/
425
+ llm/ messages, tools, agentic loop (no GenerationClient)
397
426
 
398
427
  events/ log_monitor/ built on the capabilities above
399
428
  ```
@@ -2,7 +2,8 @@
2
2
 
3
3
  Shared foundations for Python projects: structured logging, benchmarking,
4
4
  registries, a FastAPI application with routers and handlers, SQL statements and
5
- migrations, background tasks, an in-memory record store, and ETL scaffolding.
5
+ migrations, background tasks, an in-memory record store, ETL scaffolding, and
6
+ OpenAI-shaped chat / tool-loop primitives.
6
7
 
7
8
  Requires Python 3.11+.
8
9
 
@@ -20,7 +21,7 @@ extras to remember, and no import that fails because something was left out.
20
21
  Pin a compatible release rather than tracking whatever is newest:
21
22
 
22
23
  ```
23
- python-corekit~=0.3.0
24
+ python-corekit~=0.4.0
24
25
  ```
25
26
 
26
27
  Before 1.0, the minor version carries breaking changes.
@@ -246,6 +247,32 @@ ceiling, not 9,999 threads.
246
247
  Failures propagate by default. Pass `raise_on_error=False` to log and skip them
247
248
  instead, which loses results silently and so is opt-in.
248
249
 
250
+ ## LLM chat and tools
251
+
252
+ OpenAI-shaped messages, a tool registry, an agentic loop, and a thin
253
+ ``ChatCompletionsClient`` (a ``BaseApiClient``) for any OpenAI-compatible
254
+ server — llama.cpp, vLLM, OpenAI, and so on. Not a multi-provider generation
255
+ facade.
256
+
257
+ ```python
258
+ from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
259
+
260
+ client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
261
+ prompts = PromptLoader("/path/to/prompts") # mount any compatible tree
262
+ tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
263
+
264
+ registry = ToolRegistry()
265
+ registry.register(MyTool())
266
+ loop = ToolLoop(client, registry)
267
+ async for event in loop.stream(tmpl.messages(text="…", style="brief", length="short")):
268
+ ...
269
+ ```
270
+
271
+ ``PromptLoader`` reads versioned ``.txt`` files from a directory you point at
272
+ (``{kind}/{name}/{role}-v{version}.txt``, plus ``common/`` fragments). Prompt
273
+ *text* stays with the consumer — corekit only ships the loader. See
274
+ `docs/LLM_EXTRACTION.md`.
275
+
249
276
  ## HTTP clients
250
277
 
251
278
  ```python
@@ -359,6 +386,7 @@ corekit/
359
386
 
360
387
  api/ Application, lifespan, middleware, routers
361
388
  docker/ notifications/ etl/
389
+ llm/ messages, tools, agentic loop (no GenerationClient)
362
390
 
363
391
  events/ log_monitor/ built on the capabilities above
364
392
  ```
@@ -27,6 +27,7 @@ from corekit.http.exceptions import (
27
27
  from corekit.http.exponential_backoff import ExponentialBackoff
28
28
  from corekit.http.response import BaseApiResponse
29
29
  from corekit.http.status import HTTPStatusCode
30
+ from corekit.http.stream import SseDecoder, SseJsonDecoder, SseTextDecoder, StreamDecoder
30
31
 
31
32
  __all__ = [
32
33
  "BadGatewayException",
@@ -42,6 +43,10 @@ __all__ = [
42
43
  "InternalServerErrorException",
43
44
  "NotFoundException",
44
45
  "ServiceUnavailableException",
46
+ "SseDecoder",
47
+ "SseJsonDecoder",
48
+ "SseTextDecoder",
49
+ "StreamDecoder",
45
50
  "TooManyRequestsException",
46
51
  "UnauthorizedException",
47
52
  "UnprocessableEntityException",
@@ -2,9 +2,10 @@
2
2
  A small HTTP client with retries.
3
3
 
4
4
  ``BaseHttpClient`` is the transport: relative paths, optional strict URL
5
- checking, and retries for statuses the typed HTTP exceptions mark as
6
- retryable. Subclass ``BaseApiClient`` when the client always talks to one
7
- API and a foreign absolute URL should be a bug.
5
+ checking, retries for statuses the typed HTTP exceptions mark as
6
+ retryable, and ``stream_request`` for SSE / chunked bodies. Subclass
7
+ ``BaseApiClient`` when the client always talks to one API and a foreign
8
+ absolute URL should be a bug.
8
9
 
9
10
  class GithubClient(BaseApiClient):
10
11
  '''
@@ -27,6 +28,8 @@ Every response comes back as a ``BaseApiResponse``, so callers see one shape
27
28
  regardless of what the endpoint returned.
28
29
  """
29
30
 
31
+ from collections.abc import AsyncIterator
32
+ from contextlib import asynccontextmanager
30
33
  from http import HTTPMethod
31
34
  from typing import Any
32
35
  from urllib.parse import urljoin, urlsplit, urlunsplit
@@ -182,6 +185,34 @@ class BaseHttpClient(Benchmarkable):
182
185
  self.warning(f"{method} {_without_query(target)} returned {raw.status_code}; retrying")
183
186
  await backoff.async_wait()
184
187
 
188
+ @asynccontextmanager
189
+ async def stream_request(self, method: HTTPMethod, url: str, **kwargs: Any) -> AsyncIterator[httpx.Response]:
190
+ """
191
+ Open a streaming response and yield the raw ``httpx.Response``.
192
+
193
+ Holds the connection for the life of the ``async with`` block. Unlike
194
+ ``request``, the body is not buffered into a ``BaseApiResponse`` — the
195
+ caller reads it (for example with ``aiter_lines`` for SSE). Retries are
196
+ not applied: a mid-stream failure cannot be replayed safely.
197
+ """
198
+ if method not in self.supported_methods:
199
+ raise UnsupportedMethodError(method)
200
+
201
+ target = self._build_url(url)
202
+ headers = dict(self.headers)
203
+ extra_headers: dict[str, Any] | None = kwargs.pop("headers", None)
204
+ if extra_headers:
205
+ headers.update(extra_headers)
206
+
207
+ if self._http_client is not None:
208
+ async with self._http_client.stream(str(method), target, headers=headers, **kwargs) as response:
209
+ yield response
210
+ return
211
+
212
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
213
+ async with client.stream(str(method), target, headers=headers, **kwargs) as response:
214
+ yield response
215
+
185
216
  @allow_sync
186
217
  async def get(self, url: str, **kwargs: Any) -> BaseApiResponse:
187
218
  return await self.request(HTTPMethod.GET, url, **kwargs)
@@ -0,0 +1,110 @@
1
+ """
2
+ Decode streaming HTTP response bodies into typed events.
3
+
4
+ ``BaseHttpClient.stream_request`` yields a raw ``httpx.Response``. Decoders
5
+ turn that into an async iterator of useful values (SSE text, SSE JSON, …).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import AsyncIterator
13
+ from typing import Any, Generic, TypeVar
14
+
15
+ import httpx
16
+
17
+ from corekit.observability import Loggable
18
+
19
+ __all__ = [
20
+ "SseDecoder",
21
+ "SseJsonDecoder",
22
+ "SseTextDecoder",
23
+ "StreamDecoder",
24
+ ]
25
+
26
+ T = TypeVar("T")
27
+
28
+
29
+ class StreamDecoder(Loggable, ABC, Generic[T]):
30
+ """
31
+ Turn a streaming ``httpx.Response`` into an async iterator of ``T``.
32
+ """
33
+
34
+ @abstractmethod
35
+ def decode(self, response: httpx.Response) -> AsyncIterator[T]:
36
+ """
37
+ Consume ``response`` and yield decoded items.
38
+
39
+ Implemented as an async generator on concrete subclasses.
40
+ """
41
+ ...
42
+
43
+
44
+ class SseDecoder(StreamDecoder[T], ABC):
45
+ """
46
+ Server-Sent Events framing: ``data:`` lines, skip comments / blanks.
47
+
48
+ Optional ``done_sentinel`` (e.g. OpenAI's ``[DONE]``) ends the stream
49
+ early. Subclasses interpret each data payload via ``_decode_payload``.
50
+ """
51
+
52
+ def __init__(self, *, done_sentinel: str | None = None) -> None:
53
+ super().__init__()
54
+ self.done_sentinel = done_sentinel
55
+
56
+ async def decode(self, response: httpx.Response) -> AsyncIterator[T]:
57
+ async for data in self._iter_data_payloads(response):
58
+ item = self._decode_payload(data)
59
+ if item is not None:
60
+ yield item
61
+
62
+ async def _iter_data_payloads(self, response: httpx.Response) -> AsyncIterator[str]:
63
+ async for line in response.aiter_lines():
64
+ line = line.strip()
65
+ if not line.startswith("data:"):
66
+ continue
67
+ data = line.removeprefix("data:").strip()
68
+ if not data:
69
+ continue
70
+ if self.done_sentinel is not None and data == self.done_sentinel:
71
+ return
72
+ yield data
73
+
74
+ @abstractmethod
75
+ def _decode_payload(self, data: str) -> T | None:
76
+ """
77
+ Map one SSE data payload to ``T``, or ``None`` to skip.
78
+ """
79
+
80
+
81
+ class SseTextDecoder(SseDecoder[str]):
82
+ """
83
+ Yield SSE data payloads as plain strings.
84
+ """
85
+
86
+ def _decode_payload(self, data: str) -> str | None:
87
+ return data
88
+
89
+
90
+ class SseJsonDecoder(SseDecoder[dict[str, Any]]):
91
+ """
92
+ Yield SSE data payloads parsed as JSON objects.
93
+
94
+ Non-object JSON and decode failures are skipped (with a warning). Default
95
+ ``done_sentinel`` matches OpenAI-compatible chat streams.
96
+ """
97
+
98
+ def __init__(self, *, done_sentinel: str | None = "[DONE]") -> None:
99
+ super().__init__(done_sentinel=done_sentinel)
100
+
101
+ def _decode_payload(self, data: str) -> dict[str, Any] | None:
102
+ try:
103
+ payload = json.loads(data)
104
+ except json.JSONDecodeError:
105
+ self.warning(f"[SseJsonDecoder] Skipping bad SSE payload: {data!r}")
106
+ return None
107
+ if isinstance(payload, dict):
108
+ return payload
109
+ self.warning(f"[SseJsonDecoder] Skipping non-object SSE JSON: {type(payload).__name__}")
110
+ return None
@@ -0,0 +1,134 @@
1
+ """
2
+ OpenAI-shaped chat primitives, tool registry, and agentic loop.
3
+
4
+ Wire format in, structured stream events out. Use ``ChatCompletionsClient``
5
+ against any OpenAI-compatible server (llama.cpp, vLLM, OpenAI, …):
6
+
7
+ from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
8
+
9
+ client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
10
+ prompts = PromptLoader("/path/to/prompts")
11
+ tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
12
+ registry = ToolRegistry()
13
+ registry.register(MyTool())
14
+ loop = ToolLoop(client, registry)
15
+ async for event in loop.stream(tmpl.messages(text="…", style="brief", length="1 paragraph")):
16
+ ...
17
+ """
18
+
19
+ from corekit.llm.client import DEFAULT_LLM_TIMEOUT, ChatCompletionsClient
20
+ from corekit.llm.enum import (
21
+ ContentPartType,
22
+ FinishReason,
23
+ Role,
24
+ StopReason,
25
+ StreamEventType,
26
+ ToolCallType,
27
+ ToolKind,
28
+ WireField,
29
+ )
30
+ from corekit.llm.events import (
31
+ DoneEvent,
32
+ StopEvent,
33
+ StreamEvent,
34
+ TextEvent,
35
+ ThinkingEvent,
36
+ ToolCallEvent,
37
+ ToolResultEvent,
38
+ UIComponentEvent,
39
+ UsageEvent,
40
+ )
41
+ from corekit.llm.messages import (
42
+ AssistantMessage,
43
+ ChatTurn,
44
+ Message,
45
+ SystemMessage,
46
+ ToolMessage,
47
+ UserMessage,
48
+ )
49
+ from corekit.llm.prompts import (
50
+ DEFAULT_PROMPT_VERSION,
51
+ PromptError,
52
+ PromptFileRole,
53
+ PromptKind,
54
+ PromptLoader,
55
+ PromptNotFoundError,
56
+ PromptTemplate,
57
+ ThinkingLevel,
58
+ )
59
+ from corekit.llm.protocols import ChatClient, MetricsRecorder
60
+ from corekit.llm.streaming import (
61
+ THINK_PATTERN,
62
+ ModelStreamingState,
63
+ ThinkingMarker,
64
+ ThinkingMarkerPair,
65
+ strip_thinking,
66
+ )
67
+ from corekit.llm.tools.base import Tool, ToolCall, ToolResult
68
+ from corekit.llm.tools.detection import MAX_TOOL_ITERATIONS, ToolCallLoopDetector
69
+ from corekit.llm.tools.loop import ToolLoop
70
+ from corekit.llm.tools.registry import ToolRegistry
71
+ from corekit.llm.wire import (
72
+ ChoiceDelta,
73
+ CompletionTurn,
74
+ FunctionCallDelta,
75
+ ToolCallAccumulator,
76
+ ToolCallDelta,
77
+ UsageInfo,
78
+ )
79
+
80
+ __all__ = [
81
+ "DEFAULT_LLM_TIMEOUT",
82
+ "DEFAULT_PROMPT_VERSION",
83
+ "MAX_TOOL_ITERATIONS",
84
+ "THINK_PATTERN",
85
+ "AssistantMessage",
86
+ "ChatClient",
87
+ "ChatCompletionsClient",
88
+ "ChatTurn",
89
+ "ChoiceDelta",
90
+ "CompletionTurn",
91
+ "ContentPartType",
92
+ "DoneEvent",
93
+ "FinishReason",
94
+ "FunctionCallDelta",
95
+ "Message",
96
+ "MetricsRecorder",
97
+ "ModelStreamingState",
98
+ "PromptError",
99
+ "PromptFileRole",
100
+ "PromptKind",
101
+ "PromptLoader",
102
+ "PromptNotFoundError",
103
+ "PromptTemplate",
104
+ "Role",
105
+ "StopEvent",
106
+ "StopReason",
107
+ "StreamEvent",
108
+ "StreamEventType",
109
+ "SystemMessage",
110
+ "TextEvent",
111
+ "ThinkingEvent",
112
+ "ThinkingLevel",
113
+ "ThinkingMarker",
114
+ "ThinkingMarkerPair",
115
+ "Tool",
116
+ "ToolCall",
117
+ "ToolCallAccumulator",
118
+ "ToolCallDelta",
119
+ "ToolCallEvent",
120
+ "ToolCallLoopDetector",
121
+ "ToolCallType",
122
+ "ToolKind",
123
+ "ToolLoop",
124
+ "ToolMessage",
125
+ "ToolRegistry",
126
+ "ToolResult",
127
+ "ToolResultEvent",
128
+ "UIComponentEvent",
129
+ "UsageEvent",
130
+ "UsageInfo",
131
+ "UserMessage",
132
+ "WireField",
133
+ "strip_thinking",
134
+ ]
@@ -0,0 +1,179 @@
1
+ """
2
+ OpenAI-compatible chat completions client.
3
+
4
+ A thin ``BaseApiClient`` for ``/chat/completions``. Transport and status
5
+ handling come from the HTTP layer; streaming bodies go through
6
+ ``SseJsonDecoder``. This module only knows the OpenAI request wire shape.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import AsyncIterator, Coroutine, Mapping, Sequence
12
+ from http import HTTPMethod
13
+ from typing import Any, Literal, overload
14
+
15
+ from corekit.concurrency.decorators import allow_sync
16
+ from corekit.http import BaseApiClient, BaseApiResponse, SseJsonDecoder
17
+ from corekit.llm.messages import ChatTurn, Message
18
+
19
+ __all__ = ["ChatCompletionsClient", "DEFAULT_LLM_TIMEOUT"]
20
+
21
+ # LLM calls can run for minutes; the HTTP default of 30s is too short.
22
+ DEFAULT_LLM_TIMEOUT = 600.0
23
+
24
+ # Owned by this client — sampling kwargs and friends pass through untouched.
25
+ _BODY_RESERVED = frozenset({"model", "messages", "stream", "stream_options", "tools", "tool_choice"})
26
+
27
+
28
+ class ChatCompletionsClient(BaseApiClient):
29
+ """
30
+ OpenAI-shaped chat completions over HTTP.
31
+
32
+ client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
33
+
34
+ # one-shot: a dict from sync code, a coroutine inside a running loop
35
+ result = client.complete([UserMessage(content="hi")])
36
+ result = await client.complete([UserMessage(content="hi")])
37
+
38
+ # streaming is always an async iterator — there is no sync equivalent
39
+ async for chunk in client.complete(messages, stream=True):
40
+ ...
41
+
42
+ ``base_url`` should include the API root (typically ``…/v1``). Accepts
43
+ ``Message`` instances or raw OpenAI message dicts.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ base_url: str,
49
+ *,
50
+ api_key: str | None = None,
51
+ model: str | None = None,
52
+ timeout: float = DEFAULT_LLM_TIMEOUT,
53
+ **kwargs: Any,
54
+ ) -> None:
55
+ self._api_key = api_key
56
+ self._model = model
57
+ # Trailing slash so urljoin keeps the ``/v1`` segment for relative paths.
58
+ self._api_base = base_url if base_url.endswith("/") else f"{base_url}/"
59
+ self._stream_decoder = SseJsonDecoder()
60
+ super().__init__(timeout=timeout, **kwargs)
61
+
62
+ @property
63
+ def base_url(self) -> str:
64
+ return self._api_base
65
+
66
+ @property
67
+ def headers(self) -> dict[str, str]:
68
+ if not self._api_key:
69
+ return {}
70
+ return {"Authorization": f"Bearer {self._api_key}"}
71
+
72
+ def _resolve_model(self, model: str | None) -> str:
73
+ resolved = model if model is not None else self._model
74
+ if not resolved:
75
+ raise ValueError("model is required (pass model= to complete() or the constructor)")
76
+ return resolved
77
+
78
+ def _build_body(
79
+ self,
80
+ messages: Sequence[ChatTurn],
81
+ *,
82
+ stream: bool,
83
+ tools: Sequence[Mapping[str, Any]] | None = None,
84
+ model: str | None = None,
85
+ internal: bool = False,
86
+ **params: Any,
87
+ ) -> dict[str, Any]:
88
+ body: dict[str, Any] = {
89
+ "model": self._resolve_model(model),
90
+ "messages": Message.as_wire(messages, internal=internal),
91
+ "stream": stream,
92
+ }
93
+ if stream:
94
+ body["stream_options"] = {"include_usage": True}
95
+ if tools:
96
+ body["tools"] = list(tools)
97
+ body["tool_choice"] = params.pop("tool_choice", "auto")
98
+ for key, value in params.items():
99
+ if key not in _BODY_RESERVED:
100
+ body[key] = value
101
+ return body
102
+
103
+ @overload
104
+ def complete(
105
+ self,
106
+ messages: Sequence[ChatTurn],
107
+ *,
108
+ tools: Sequence[Mapping[str, Any]] | None = None,
109
+ model: str | None = None,
110
+ stream: Literal[False] = False,
111
+ internal: bool = False,
112
+ **kwargs: Any,
113
+ ) -> dict[str, Any] | Coroutine[Any, Any, dict[str, Any]]: ...
114
+
115
+ @overload
116
+ def complete(
117
+ self,
118
+ messages: Sequence[ChatTurn],
119
+ *,
120
+ tools: Sequence[Mapping[str, Any]] | None = None,
121
+ model: str | None = None,
122
+ stream: Literal[True],
123
+ internal: bool = False,
124
+ **kwargs: Any,
125
+ ) -> AsyncIterator[dict[str, Any]]: ...
126
+
127
+ def complete(
128
+ self,
129
+ messages: Sequence[ChatTurn],
130
+ *,
131
+ tools: Sequence[Mapping[str, Any]] | None = None,
132
+ model: str | None = None,
133
+ stream: bool = False,
134
+ internal: bool = False,
135
+ **kwargs: Any,
136
+ ) -> dict[str, Any] | Coroutine[Any, Any, dict[str, Any]] | AsyncIterator[dict[str, Any]]:
137
+ """
138
+ POST ``/chat/completions``.
139
+
140
+ ``stream=False`` (default) uses ``@allow_sync``: a script gets the
141
+ response dict back; inside a running event loop you must ``await`` it.
142
+ ``stream=True`` is always an async iterator — ``@allow_sync`` only
143
+ unwraps one awaitable, and buffering a stream would defeat streaming.
144
+ """
145
+ body = self._build_body(
146
+ messages,
147
+ stream=stream,
148
+ tools=tools,
149
+ model=model,
150
+ internal=internal,
151
+ **kwargs,
152
+ )
153
+ if stream:
154
+ return self._complete_stream(body)
155
+ return self._complete(body)
156
+
157
+ @allow_sync
158
+ async def _complete(self, body: dict[str, Any]) -> dict[str, Any]:
159
+ self.info(
160
+ f"[ChatCompletionsClient] Completing {body['model']!r} — "
161
+ f"messages={len(body['messages'])}, tools={'tools' in body}"
162
+ )
163
+ response = await self.post("chat/completions", json=body)
164
+ response.raise_for_status()
165
+ if not response.data:
166
+ raise ValueError(f"chat/completions returned non-JSON body: {response.text!r}")
167
+ return dict(response.data)
168
+
169
+ async def _complete_stream(self, body: dict[str, Any]) -> AsyncIterator[dict[str, Any]]:
170
+ self.info(
171
+ f"[ChatCompletionsClient] Streaming {body['model']!r} — "
172
+ f"messages={len(body['messages'])}, tools={'tools' in body}"
173
+ )
174
+ async with self.stream_request(HTTPMethod.POST, "chat/completions", json=body) as response:
175
+ if int(response.status_code) >= 400:
176
+ error_body = (await response.aread()).decode("utf-8", errors="replace")
177
+ BaseApiResponse(status_code=response.status_code, text=error_body).raise_for_status()
178
+ async for chunk in self._stream_decoder.decode(response):
179
+ yield chunk