ommlds 0.0.0.dev456__py3-none-any.whl → 0.0.0.dev485__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 (212) hide show
  1. ommlds/.omlish-manifests.json +314 -33
  2. ommlds/__about__.py +15 -9
  3. ommlds/_hacks/__init__.py +4 -0
  4. ommlds/_hacks/funcs.py +110 -0
  5. ommlds/_hacks/names.py +158 -0
  6. ommlds/_hacks/params.py +73 -0
  7. ommlds/_hacks/patches.py +0 -3
  8. ommlds/backends/anthropic/protocol/__init__.py +13 -1
  9. ommlds/backends/anthropic/protocol/_dataclasses.py +1625 -0
  10. ommlds/backends/anthropic/protocol/sse/assemble.py +22 -6
  11. ommlds/backends/anthropic/protocol/sse/events.py +13 -0
  12. ommlds/backends/google/protocol/__init__.py +13 -0
  13. ommlds/backends/google/protocol/_dataclasses.py +5997 -0
  14. ommlds/backends/google/protocol/types.py +5 -1
  15. ommlds/backends/groq/__init__.py +7 -0
  16. ommlds/backends/groq/_dataclasses.py +3901 -0
  17. ommlds/backends/groq/_marshal.py +23 -0
  18. ommlds/backends/groq/protocol.py +249 -0
  19. ommlds/backends/llamacpp/logging.py +4 -1
  20. ommlds/backends/mlx/caching.py +7 -3
  21. ommlds/backends/mlx/cli.py +10 -7
  22. ommlds/backends/mlx/generation.py +18 -16
  23. ommlds/backends/mlx/limits.py +10 -6
  24. ommlds/backends/mlx/loading.py +65 -5
  25. ommlds/backends/ollama/__init__.py +7 -0
  26. ommlds/backends/ollama/_dataclasses.py +3458 -0
  27. ommlds/backends/ollama/protocol.py +170 -0
  28. ommlds/backends/openai/protocol/__init__.py +15 -1
  29. ommlds/backends/openai/protocol/_dataclasses.py +7708 -0
  30. ommlds/backends/tavily/__init__.py +7 -0
  31. ommlds/backends/tavily/_dataclasses.py +1734 -0
  32. ommlds/backends/tavily/protocol.py +301 -0
  33. ommlds/backends/tinygrad/models/llama3/__init__.py +22 -14
  34. ommlds/backends/transformers/__init__.py +14 -0
  35. ommlds/backends/transformers/filecache.py +109 -0
  36. ommlds/backends/transformers/streamers.py +73 -0
  37. ommlds/cli/__init__.py +7 -0
  38. ommlds/cli/_dataclasses.py +2562 -0
  39. ommlds/cli/asyncs.py +30 -0
  40. ommlds/cli/backends/catalog.py +93 -0
  41. ommlds/cli/backends/configs.py +9 -0
  42. ommlds/cli/backends/inject.py +31 -36
  43. ommlds/cli/backends/injection.py +16 -0
  44. ommlds/cli/backends/types.py +46 -0
  45. ommlds/cli/content/messages.py +34 -0
  46. ommlds/cli/content/strings.py +42 -0
  47. ommlds/cli/inject.py +15 -32
  48. ommlds/cli/inputs/__init__.py +0 -0
  49. ommlds/cli/inputs/asyncs.py +32 -0
  50. ommlds/cli/inputs/sync.py +75 -0
  51. ommlds/cli/main.py +267 -128
  52. ommlds/cli/rendering/__init__.py +0 -0
  53. ommlds/cli/rendering/configs.py +9 -0
  54. ommlds/cli/rendering/inject.py +31 -0
  55. ommlds/cli/rendering/markdown.py +52 -0
  56. ommlds/cli/rendering/raw.py +73 -0
  57. ommlds/cli/rendering/types.py +21 -0
  58. ommlds/cli/secrets.py +21 -0
  59. ommlds/cli/sessions/base.py +1 -1
  60. ommlds/cli/sessions/chat/chat/__init__.py +0 -0
  61. ommlds/cli/sessions/chat/chat/ai/__init__.py +0 -0
  62. ommlds/cli/sessions/chat/chat/ai/configs.py +11 -0
  63. ommlds/cli/sessions/chat/chat/ai/inject.py +74 -0
  64. ommlds/cli/sessions/chat/chat/ai/injection.py +14 -0
  65. ommlds/cli/sessions/chat/chat/ai/rendering.py +70 -0
  66. ommlds/cli/sessions/chat/chat/ai/services.py +79 -0
  67. ommlds/cli/sessions/chat/chat/ai/tools.py +44 -0
  68. ommlds/cli/sessions/chat/chat/ai/types.py +28 -0
  69. ommlds/cli/sessions/chat/chat/state/__init__.py +0 -0
  70. ommlds/cli/sessions/chat/chat/state/configs.py +11 -0
  71. ommlds/cli/sessions/chat/chat/state/inject.py +36 -0
  72. ommlds/cli/sessions/chat/chat/state/inmemory.py +33 -0
  73. ommlds/cli/sessions/chat/chat/state/storage.py +52 -0
  74. ommlds/cli/sessions/chat/chat/state/types.py +38 -0
  75. ommlds/cli/sessions/chat/chat/user/__init__.py +0 -0
  76. ommlds/cli/sessions/chat/chat/user/configs.py +17 -0
  77. ommlds/cli/sessions/chat/chat/user/inject.py +62 -0
  78. ommlds/cli/sessions/chat/chat/user/interactive.py +31 -0
  79. ommlds/cli/sessions/chat/chat/user/oneshot.py +25 -0
  80. ommlds/cli/sessions/chat/chat/user/types.py +15 -0
  81. ommlds/cli/sessions/chat/configs.py +27 -0
  82. ommlds/cli/sessions/chat/driver.py +43 -0
  83. ommlds/cli/sessions/chat/inject.py +33 -65
  84. ommlds/cli/sessions/chat/phases/__init__.py +0 -0
  85. ommlds/cli/sessions/chat/phases/inject.py +27 -0
  86. ommlds/cli/sessions/chat/phases/injection.py +14 -0
  87. ommlds/cli/sessions/chat/phases/manager.py +29 -0
  88. ommlds/cli/sessions/chat/phases/types.py +29 -0
  89. ommlds/cli/sessions/chat/session.py +27 -0
  90. ommlds/cli/sessions/chat/tools/__init__.py +0 -0
  91. ommlds/cli/sessions/chat/tools/configs.py +22 -0
  92. ommlds/cli/sessions/chat/tools/confirmation.py +46 -0
  93. ommlds/cli/sessions/chat/tools/execution.py +66 -0
  94. ommlds/cli/sessions/chat/tools/fs/__init__.py +0 -0
  95. ommlds/cli/sessions/chat/tools/fs/configs.py +12 -0
  96. ommlds/cli/sessions/chat/tools/fs/inject.py +35 -0
  97. ommlds/cli/sessions/chat/tools/inject.py +88 -0
  98. ommlds/cli/sessions/chat/tools/injection.py +44 -0
  99. ommlds/cli/sessions/chat/tools/rendering.py +58 -0
  100. ommlds/cli/sessions/chat/tools/todo/__init__.py +0 -0
  101. ommlds/cli/sessions/chat/tools/todo/configs.py +12 -0
  102. ommlds/cli/sessions/chat/tools/todo/inject.py +31 -0
  103. ommlds/cli/sessions/chat/tools/weather/__init__.py +0 -0
  104. ommlds/cli/sessions/chat/tools/weather/configs.py +12 -0
  105. ommlds/cli/sessions/chat/tools/weather/inject.py +22 -0
  106. ommlds/cli/{tools/weather.py → sessions/chat/tools/weather/tools.py} +1 -1
  107. ommlds/cli/sessions/completion/configs.py +21 -0
  108. ommlds/cli/sessions/completion/inject.py +42 -0
  109. ommlds/cli/sessions/completion/session.py +35 -0
  110. ommlds/cli/sessions/embedding/configs.py +21 -0
  111. ommlds/cli/sessions/embedding/inject.py +42 -0
  112. ommlds/cli/sessions/embedding/session.py +33 -0
  113. ommlds/cli/sessions/inject.py +28 -11
  114. ommlds/cli/state/__init__.py +0 -0
  115. ommlds/cli/state/inject.py +28 -0
  116. ommlds/cli/{state.py → state/storage.py} +41 -24
  117. ommlds/minichain/__init__.py +46 -17
  118. ommlds/minichain/_dataclasses.py +15401 -0
  119. ommlds/minichain/backends/catalogs/base.py +20 -1
  120. ommlds/minichain/backends/catalogs/simple.py +2 -2
  121. ommlds/minichain/backends/catalogs/strings.py +10 -8
  122. ommlds/minichain/backends/impls/anthropic/chat.py +31 -65
  123. ommlds/minichain/backends/impls/anthropic/names.py +3 -4
  124. ommlds/minichain/backends/impls/anthropic/protocol.py +109 -0
  125. ommlds/minichain/backends/impls/anthropic/stream.py +53 -31
  126. ommlds/minichain/backends/impls/duckduckgo/search.py +5 -1
  127. ommlds/minichain/backends/impls/dummy/__init__.py +0 -0
  128. ommlds/minichain/backends/impls/dummy/chat.py +69 -0
  129. ommlds/minichain/backends/impls/google/chat.py +9 -2
  130. ommlds/minichain/backends/impls/google/search.py +6 -1
  131. ommlds/minichain/backends/impls/google/stream.py +122 -32
  132. ommlds/minichain/backends/impls/groq/__init__.py +0 -0
  133. ommlds/minichain/backends/impls/groq/chat.py +75 -0
  134. ommlds/minichain/backends/impls/groq/names.py +48 -0
  135. ommlds/minichain/backends/impls/groq/protocol.py +143 -0
  136. ommlds/minichain/backends/impls/groq/stream.py +125 -0
  137. ommlds/minichain/backends/impls/huggingface/repos.py +1 -5
  138. ommlds/minichain/backends/impls/llamacpp/chat.py +15 -3
  139. ommlds/minichain/backends/impls/llamacpp/completion.py +7 -3
  140. ommlds/minichain/backends/impls/llamacpp/stream.py +38 -19
  141. ommlds/minichain/backends/impls/mistral.py +9 -2
  142. ommlds/minichain/backends/impls/mlx/chat.py +100 -23
  143. ommlds/minichain/backends/impls/ollama/__init__.py +0 -0
  144. ommlds/minichain/backends/impls/ollama/chat.py +199 -0
  145. ommlds/minichain/backends/impls/openai/chat.py +14 -7
  146. ommlds/minichain/backends/impls/openai/completion.py +9 -2
  147. ommlds/minichain/backends/impls/openai/embedding.py +9 -2
  148. ommlds/minichain/backends/impls/openai/format.py +115 -109
  149. ommlds/minichain/backends/impls/openai/names.py +31 -5
  150. ommlds/minichain/backends/impls/openai/stream.py +33 -27
  151. ommlds/minichain/backends/impls/sentencepiece/tokens.py +9 -6
  152. ommlds/minichain/backends/impls/tavily.py +66 -0
  153. ommlds/minichain/backends/impls/tinygrad/chat.py +17 -14
  154. ommlds/minichain/backends/impls/tokenizers/tokens.py +9 -6
  155. ommlds/minichain/backends/impls/transformers/sentence.py +5 -2
  156. ommlds/minichain/backends/impls/transformers/tokens.py +10 -7
  157. ommlds/minichain/backends/impls/transformers/transformers.py +139 -20
  158. ommlds/minichain/backends/strings/parsing.py +1 -1
  159. ommlds/minichain/backends/strings/resolving.py +4 -1
  160. ommlds/minichain/chat/choices/stream/__init__.py +0 -0
  161. ommlds/minichain/chat/choices/stream/adapters.py +35 -0
  162. ommlds/minichain/chat/choices/stream/joining.py +31 -0
  163. ommlds/minichain/chat/choices/stream/services.py +45 -0
  164. ommlds/minichain/chat/choices/stream/types.py +43 -0
  165. ommlds/minichain/chat/stream/_marshal.py +4 -4
  166. ommlds/minichain/chat/stream/joining.py +85 -0
  167. ommlds/minichain/chat/stream/services.py +15 -15
  168. ommlds/minichain/chat/stream/types.py +24 -18
  169. ommlds/minichain/llms/types.py +4 -0
  170. ommlds/minichain/registries/globals.py +18 -4
  171. ommlds/minichain/resources.py +28 -3
  172. ommlds/minichain/search.py +1 -1
  173. ommlds/minichain/standard.py +8 -0
  174. ommlds/minichain/stream/services.py +19 -16
  175. ommlds/minichain/tools/reflect.py +5 -1
  176. ommlds/nanochat/LICENSE +21 -0
  177. ommlds/nanochat/__init__.py +0 -0
  178. ommlds/nanochat/rustbpe/LICENSE +21 -0
  179. ommlds/nanochat/tokenizers.py +406 -0
  180. ommlds/specs/__init__.py +0 -0
  181. ommlds/specs/mcp/__init__.py +0 -0
  182. ommlds/specs/mcp/_marshal.py +23 -0
  183. ommlds/specs/mcp/clients.py +146 -0
  184. ommlds/specs/mcp/protocol.py +371 -0
  185. ommlds/tools/git.py +13 -6
  186. ommlds/tools/ocr.py +1 -8
  187. ommlds/wiki/analyze.py +2 -2
  188. ommlds/wiki/text/mfh.py +1 -5
  189. ommlds/wiki/text/wtp.py +1 -3
  190. ommlds/wiki/utils/xml.py +5 -5
  191. {ommlds-0.0.0.dev456.dist-info → ommlds-0.0.0.dev485.dist-info}/METADATA +22 -19
  192. {ommlds-0.0.0.dev456.dist-info → ommlds-0.0.0.dev485.dist-info}/RECORD +198 -95
  193. ommlds/cli/backends/standard.py +0 -20
  194. ommlds/cli/sessions/chat/base.py +0 -42
  195. ommlds/cli/sessions/chat/code.py +0 -129
  196. ommlds/cli/sessions/chat/interactive.py +0 -71
  197. ommlds/cli/sessions/chat/printing.py +0 -97
  198. ommlds/cli/sessions/chat/prompt.py +0 -151
  199. ommlds/cli/sessions/chat/state.py +0 -110
  200. ommlds/cli/sessions/chat/tools.py +0 -100
  201. ommlds/cli/sessions/completion/completion.py +0 -44
  202. ommlds/cli/sessions/embedding/embedding.py +0 -42
  203. ommlds/cli/tools/config.py +0 -14
  204. ommlds/cli/tools/inject.py +0 -75
  205. ommlds/minichain/backends/impls/openai/format2.py +0 -210
  206. ommlds/minichain/chat/stream/adapters.py +0 -80
  207. /ommlds/{huggingface.py → backends/huggingface.py} +0 -0
  208. /ommlds/cli/{tools → content}/__init__.py +0 -0
  209. {ommlds-0.0.0.dev456.dist-info → ommlds-0.0.0.dev485.dist-info}/WHEEL +0 -0
  210. {ommlds-0.0.0.dev456.dist-info → ommlds-0.0.0.dev485.dist-info}/entry_points.txt +0 -0
  211. {ommlds-0.0.0.dev456.dist-info → ommlds-0.0.0.dev485.dist-info}/licenses/LICENSE +0 -0
  212. {ommlds-0.0.0.dev456.dist-info → ommlds-0.0.0.dev485.dist-info}/top_level.txt +0 -0
@@ -3,15 +3,34 @@ import typing as ta
3
3
 
4
4
  from omlish import lang
5
5
 
6
+ from ...configs import Config
7
+
8
+
9
+ T = ta.TypeVar('T')
10
+
6
11
 
7
12
  ##
8
13
 
9
14
 
10
15
  class BackendCatalog(lang.Abstract):
16
+ class Backend(ta.NamedTuple):
17
+ factory: ta.Callable[..., ta.Any]
18
+ configs: ta.Sequence[Config] | None
19
+
11
20
  @abc.abstractmethod
12
- def get_backend(self, service_cls: ta.Any, name: str, *args: ta.Any, **kwargs: ta.Any) -> ta.Any:
21
+ def get_backend(self, service_cls: type[T], name: str) -> Backend:
13
22
  raise NotImplementedError
14
23
 
24
+ def new_backend(
25
+ self,
26
+ service_cls: ta.Any,
27
+ name: str,
28
+ *args: ta.Any,
29
+ **kwargs: ta.Any,
30
+ ) -> ta.Any:
31
+ be = self.get_backend(service_cls, name)
32
+ return be.factory(*be.configs or [], *args, **kwargs)
33
+
15
34
  # #
16
35
  #
17
36
  # class Bound(lang.Final, ta.Generic[T]):
@@ -40,9 +40,9 @@ class SimpleBackendCatalog(BackendCatalog):
40
40
  sc_dct[e.name] = e
41
41
  self._dct = dct
42
42
 
43
- def get_backend(self, service_cls: ta.Any, name: str, *args: ta.Any, **kwargs: ta.Any) -> ta.Any:
43
+ def get_backend(self, service_cls: ta.Any, name: str, *args: ta.Any, **kwargs: ta.Any) -> BackendCatalog.Backend:
44
44
  e = self._dct[service_cls][name]
45
- return e.factory_fn(*args, **kwargs)
45
+ return BackendCatalog.Backend(e.factory_fn, None)
46
46
 
47
47
 
48
48
  ##
@@ -5,7 +5,7 @@ from omlish import check
5
5
  from ...models.configs import ModelPath
6
6
  from ...models.configs import ModelRepo
7
7
  from ...models.repos.resolving import ModelRepoResolver
8
- from ...registries.globals import registry_new
8
+ from ...registries.globals import get_registry_cls
9
9
  from ..strings.parsing import parse_backend_string
10
10
  from ..strings.resolving import BackendStringResolver
11
11
  from ..strings.resolving import ResolveBackendStringArgs
@@ -30,26 +30,28 @@ class BackendStringBackendCatalog(BackendCatalog):
30
30
  self._string_resolver = string_resolver
31
31
  self._model_repo_resolver = model_repo_resolver
32
32
 
33
- def get_backend(self, service_cls: ta.Any, name: str, *args: ta.Any, **kwargs: ta.Any) -> ta.Any:
33
+ def get_backend(self, service_cls: ta.Any, name: str, *args: ta.Any, **kwargs: ta.Any) -> BackendCatalog.Backend:
34
34
  ps = parse_backend_string(name)
35
35
  rs = check.not_none(self._string_resolver.resolve_backend_string(ResolveBackendStringArgs(
36
36
  service_cls,
37
37
  ps,
38
38
  )))
39
39
 
40
- al = list(rs.args or [])
40
+ al: list = list(rs.args or [])
41
41
 
42
- # FIXME: lol
42
+ # FIXME: lol - move *into* local model classes as an injected dep?
43
43
  if al and isinstance(al[0], ModelRepo):
44
44
  [mr] = al
45
45
  mrr = check.not_none(self._model_repo_resolver)
46
46
  mrp = check.not_none(mrr.resolve(mr))
47
47
  al = [ModelPath(mrp.path), *al[1:]]
48
48
 
49
- return registry_new(
49
+ cls = get_registry_cls(
50
50
  service_cls,
51
51
  rs.name,
52
- *al,
53
- *args,
54
- **kwargs,
52
+ )
53
+
54
+ return BackendCatalog.Backend(
55
+ cls,
56
+ al,
55
57
  )
@@ -17,20 +17,23 @@ from ....chat.choices.services import ChatChoicesRequest
17
17
  from ....chat.choices.services import ChatChoicesResponse
18
18
  from ....chat.choices.services import static_check_is_chat_choices_service
19
19
  from ....chat.choices.types import AiChoice
20
+ from ....chat.choices.types import ChatChoicesOptions
20
21
  from ....chat.messages import AiMessage
21
22
  from ....chat.messages import AnyAiMessage
22
23
  from ....chat.messages import Message
23
24
  from ....chat.messages import SystemMessage
24
25
  from ....chat.messages import ToolUseMessage
25
- from ....chat.messages import ToolUseResultMessage
26
26
  from ....chat.messages import UserMessage
27
27
  from ....chat.tools.types import Tool
28
- from ....content.prepare import prepare_content_str
28
+ from ....llms.types import MaxTokens
29
+ from ....llms.types import Temperature
29
30
  from ....models.configs import ModelName
30
31
  from ....standard import ApiKey
31
- from ....tools.jsonschema import build_tool_spec_params_json_schema
32
32
  from ....tools.types import ToolUse
33
+ from ....types import Option
33
34
  from .names import MODEL_NAMES
35
+ from .protocol import build_protocol_chat_messages
36
+ from .protocol import build_protocol_tool
34
37
 
35
38
 
36
39
  ##
@@ -44,19 +47,15 @@ from .names import MODEL_NAMES
44
47
  class AnthropicChatChoicesService:
45
48
  DEFAULT_MODEL_NAME: ta.ClassVar[ModelName] = ModelName(check.not_none(MODEL_NAMES.default))
46
49
 
47
- ROLES_MAP: ta.ClassVar[ta.Mapping[type[Message], str]] = {
48
- SystemMessage: 'system',
49
- UserMessage: 'user',
50
- AiMessage: 'assistant',
51
- ToolUseMessage: 'assistant',
52
- }
53
-
54
50
  def __init__(
55
51
  self,
56
52
  *configs: ApiKey | ModelName,
53
+ http_client: http.AsyncHttpClient | None = None,
57
54
  ) -> None:
58
55
  super().__init__()
59
56
 
57
+ self._http_client = http_client
58
+
60
59
  with tv.consume(*configs) as cc:
61
60
  self._api_key = check.not_none(ApiKey.pop_secret(cc, env='ANTHROPIC_API_KEY'))
62
61
  self._model_name = cc.pop(self.DEFAULT_MODEL_NAME)
@@ -72,80 +71,46 @@ class AnthropicChatChoicesService:
72
71
  else:
73
72
  raise TypeError(m)
74
73
 
74
+ DEFAULT_OPTIONS: ta.ClassVar[tv.TypedValues[Option]] = tv.TypedValues[Option](
75
+ MaxTokens(4096),
76
+ )
77
+
78
+ _OPTION_KWARG_NAMES_MAP: ta.ClassVar[ta.Mapping[str, type[ChatChoicesOptions]]] = dict(
79
+ temperature=Temperature,
80
+ max_tokens=MaxTokens,
81
+ )
82
+
75
83
  async def invoke(
76
84
  self,
77
85
  request: ChatChoicesRequest,
78
- *,
79
- max_tokens: int = 4096, # FIXME: ChatOption
80
86
  ) -> ChatChoicesResponse:
81
- messages: list[pt.Message] = []
82
- system: list[pt.Content] | None = None
83
- for i, m in enumerate(request.v):
84
- if isinstance(m, SystemMessage):
85
- if i != 0 or system is not None:
86
- raise Exception('Only supports one system message and must be first')
87
- system = [pt.Text(check.not_none(self._get_msg_content(m)))]
88
-
89
- elif isinstance(m, ToolUseResultMessage):
90
- messages.append(pt.Message(
91
- role='user',
92
- content=[pt.ToolResult(
93
- tool_use_id=check.not_none(m.tur.id),
94
- content=json.dumps_compact(msh.marshal(m.tur.c)) if not isinstance(m.tur.c, str) else m.tur.c,
95
- )],
96
- ))
87
+ messages, system = build_protocol_chat_messages(request.v)
97
88
 
98
- elif isinstance(m, AiMessage):
99
- # messages.append(pt.Message(
100
- # role=self.ROLES_MAP[type(m)], # noqa
101
- # content=[pt.Text(check.isinstance(self._get_msg_content(m), str))],
102
- # ))
103
- messages.append(pt.Message(
104
- role='assistant',
105
- content=[
106
- *([pt.Text(check.isinstance(m.c, str))] if m.c is not None else []),
107
- ],
108
- ))
109
-
110
- elif isinstance(m, ToolUseMessage):
111
- messages.append(pt.Message(
112
- role='assistant',
113
- content=[
114
- pt.ToolUse(
115
- id=check.not_none(m.tu.id),
116
- name=check.not_none(m.tu.name),
117
- input=m.tu.args,
118
- ),
119
- ],
120
- ))
121
-
122
- else:
123
- messages.append(pt.Message(
124
- role=self.ROLES_MAP[type(m)], # type: ignore[arg-type]
125
- content=[pt.Text(check.isinstance(self._get_msg_content(m), str))],
126
- ))
89
+ kwargs: dict = dict()
127
90
 
128
91
  tools: list[pt.ToolSpec] = []
129
- with tv.TypedValues(*request.options).consume() as oc:
92
+ with tv.TypedValues(
93
+ *self.DEFAULT_OPTIONS,
94
+ *request.options,
95
+ override=True,
96
+ ).consume() as oc:
97
+ kwargs.update(oc.pop_scalar_kwargs(**self._OPTION_KWARG_NAMES_MAP))
98
+
130
99
  t: Tool
131
100
  for t in oc.pop(Tool, []):
132
- tools.append(pt.ToolSpec(
133
- name=check.not_none(t.spec.name),
134
- description=prepare_content_str(t.spec.desc),
135
- input_schema=build_tool_spec_params_json_schema(t.spec),
136
- ))
101
+ tools.append(build_protocol_tool(t))
137
102
 
138
103
  a_req = pt.MessagesRequest(
139
104
  model=MODEL_NAMES.resolve(self._model_name.v),
140
105
  system=system,
141
106
  messages=messages,
142
107
  tools=tools or None,
143
- max_tokens=max_tokens,
108
+ **kwargs,
144
109
  )
145
110
 
146
111
  raw_request = msh.marshal(a_req)
147
112
 
148
- raw_response = http.request(
113
+ raw_response = await http.async_request(
149
114
  'https://api.anthropic.com/v1/messages',
150
115
  headers={
151
116
  http.consts.HEADER_CONTENT_TYPE: http.consts.CONTENT_TYPE_JSON,
@@ -153,6 +118,7 @@ class AnthropicChatChoicesService:
153
118
  b'anthropic-version': b'2023-06-01',
154
119
  },
155
120
  data=json.dumps(raw_request).encode('utf-8'),
121
+ client=self._http_client,
156
122
  )
157
123
 
158
124
  response = json.loads(check.not_none(raw_response.data).decode('utf-8'))
@@ -22,10 +22,9 @@ MODEL_NAMES = ModelNameCollection(
22
22
  'claude-sonnet-4-5': 'claude-sonnet-4-5-20250929',
23
23
  'claude-sonnet': 'claude-sonnet-4-5',
24
24
 
25
- 'claude-3-5-haiku-latest': None,
26
- 'claude-haiku-3-5-latest': 'claude-3-5-haiku-latest',
27
- 'claude-haiku-3-5': 'claude-haiku-3-5-latest',
28
- 'claude-haiku': 'claude-haiku-3-5',
25
+ 'claude-haiku-4-5-20251001': None,
26
+ 'claude-haiku-4-5': 'claude-haiku-4-5-20251001',
27
+ 'claude-haiku': 'claude-haiku-4-5',
29
28
 
30
29
  'claude': 'claude-haiku',
31
30
  },
@@ -0,0 +1,109 @@
1
+ import typing as ta
2
+
3
+ from omlish import check
4
+ from omlish import marshal as msh
5
+ from omlish.formats import json
6
+
7
+ from .....backends.anthropic.protocol import types as pt
8
+ from ....chat.messages import AiMessage
9
+ from ....chat.messages import Message
10
+ from ....chat.messages import SystemMessage
11
+ from ....chat.messages import ToolUseMessage
12
+ from ....chat.messages import ToolUseResultMessage
13
+ from ....chat.messages import UserMessage
14
+ from ....chat.tools.types import Tool
15
+ from ....content.prepare import prepare_content_str
16
+ from ....tools.jsonschema import build_tool_spec_params_json_schema
17
+
18
+
19
+ ##
20
+
21
+
22
+ def get_message_content(m: Message) -> str | None:
23
+ if isinstance(m, AiMessage):
24
+ return check.isinstance(m.c, str)
25
+
26
+ elif isinstance(m, (UserMessage, SystemMessage)):
27
+ return check.isinstance(m.c, str)
28
+
29
+ else:
30
+ raise TypeError(m)
31
+
32
+
33
+ #
34
+
35
+
36
+ class BuiltChatMessages(ta.NamedTuple):
37
+ messages: list[pt.Message]
38
+ system: list[pt.Content] | None
39
+
40
+
41
+ ROLES_MAP: ta.Mapping[type[Message], str] = {
42
+ SystemMessage: 'system',
43
+ UserMessage: 'user',
44
+ AiMessage: 'assistant',
45
+ ToolUseMessage: 'assistant',
46
+ }
47
+
48
+
49
+ def build_protocol_chat_messages(msgs: ta.Iterable[Message]) -> BuiltChatMessages:
50
+ messages: list[pt.Message] = []
51
+ system: list[pt.Content] | None = None
52
+
53
+ for i, m in enumerate(msgs):
54
+ if isinstance(m, SystemMessage):
55
+ if i or system is not None:
56
+ raise Exception('Only supports one system message and must be first')
57
+ system = [pt.Text(check.not_none(get_message_content(m)))]
58
+
59
+ elif isinstance(m, ToolUseResultMessage):
60
+ messages.append(pt.Message(
61
+ role='user',
62
+ content=[pt.ToolResult(
63
+ tool_use_id=check.not_none(m.tur.id),
64
+ content=json.dumps_compact(msh.marshal(m.tur.c)) if not isinstance(m.tur.c, str) else m.tur.c,
65
+ )],
66
+ ))
67
+
68
+ elif isinstance(m, AiMessage):
69
+ # messages.append(pt.Message(
70
+ # role=ROLES_MAP[type(m)], # noqa
71
+ # content=[pt.Text(check.isinstance(get_message_content(m), str))],
72
+ # ))
73
+ messages.append(pt.Message(
74
+ role='assistant',
75
+ content=[
76
+ *([pt.Text(check.isinstance(m.c, str))] if m.c is not None else []),
77
+ ],
78
+ ))
79
+
80
+ elif isinstance(m, ToolUseMessage):
81
+ messages.append(pt.Message(
82
+ role='assistant',
83
+ content=[
84
+ pt.ToolUse(
85
+ id=check.not_none(m.tu.id),
86
+ name=check.not_none(m.tu.name),
87
+ input=m.tu.args,
88
+ ),
89
+ ],
90
+ ))
91
+
92
+ else:
93
+ messages.append(pt.Message(
94
+ role=ROLES_MAP[type(m)], # type: ignore[arg-type]
95
+ content=[pt.Text(check.isinstance(get_message_content(m), str))],
96
+ ))
97
+
98
+ return BuiltChatMessages(messages, system)
99
+
100
+
101
+ ##
102
+
103
+
104
+ def build_protocol_tool(t: Tool) -> pt.ToolSpec:
105
+ return pt.ToolSpec(
106
+ name=check.not_none(t.spec.name),
107
+ description=prepare_content_str(t.spec.desc),
108
+ input_schema=build_tool_spec_params_json_schema(t.spec),
109
+ )
@@ -1,7 +1,6 @@
1
1
  import typing as ta
2
2
 
3
3
  from omlish import check
4
- from omlish import lang
5
4
  from omlish import marshal as msh
6
5
  from omlish import typedvalues as tv
7
6
  from omlish.formats import json
@@ -9,15 +8,17 @@ from omlish.http import all as http
9
8
  from omlish.http import sse
10
9
  from omlish.io.buffers import DelimitingBuffer
11
10
 
11
+ from .....backends.anthropic.protocol import types as pt
12
12
  from .....backends.anthropic.protocol.sse.events import AnthropicSseDecoderEvents
13
13
  from ....chat.choices.services import ChatChoicesOutputs
14
- from ....chat.messages import SystemMessage
15
- from ....chat.stream.services import ChatChoicesStreamRequest
16
- from ....chat.stream.services import ChatChoicesStreamResponse
17
- from ....chat.stream.services import static_check_is_chat_choices_stream_service
18
- from ....chat.stream.types import AiChoiceDeltas
19
- from ....chat.stream.types import AiChoicesDeltas
20
- from ....chat.stream.types import ContentAiChoiceDelta
14
+ from ....chat.choices.stream.services import ChatChoicesStreamRequest
15
+ from ....chat.choices.stream.services import ChatChoicesStreamResponse
16
+ from ....chat.choices.stream.services import static_check_is_chat_choices_stream_service
17
+ from ....chat.choices.stream.types import AiChoiceDeltas
18
+ from ....chat.choices.stream.types import AiChoicesDeltas
19
+ from ....chat.stream.types import ContentAiDelta
20
+ from ....chat.stream.types import PartialToolUseAiDelta
21
+ from ....chat.tools.types import Tool
21
22
  from ....configs import Config
22
23
  from ....resources import UseResources
23
24
  from ....standard import ApiKey
@@ -25,6 +26,8 @@ from ....stream.services import StreamResponseSink
25
26
  from ....stream.services import new_stream_response
26
27
  from .chat import AnthropicChatChoicesService
27
28
  from .names import MODEL_NAMES
29
+ from .protocol import build_protocol_chat_messages
30
+ from .protocol import build_protocol_tool
28
31
 
29
32
 
30
33
  ##
@@ -36,14 +39,20 @@ from .names import MODEL_NAMES
36
39
  # )
37
40
  @static_check_is_chat_choices_stream_service
38
41
  class AnthropicChatChoicesStreamService:
39
- def __init__(self, *configs: Config) -> None:
42
+ def __init__(
43
+ self,
44
+ *configs: Config,
45
+ http_client: http.AsyncHttpClient | None = None,
46
+ ) -> None:
40
47
  super().__init__()
41
48
 
49
+ self._http_client = http_client
50
+
42
51
  with tv.consume(*configs) as cc:
43
52
  self._model_name = cc.pop(AnthropicChatChoicesService.DEFAULT_MODEL_NAME)
44
53
  self._api_key = check.not_none(ApiKey.pop_secret(cc, env='ANTHROPIC_API_KEY'))
45
54
 
46
- READ_CHUNK_SIZE = 64 * 1024
55
+ READ_CHUNK_SIZE: ta.ClassVar[int] = -1
47
56
 
48
57
  async def invoke(
49
58
  self,
@@ -51,27 +60,25 @@ class AnthropicChatChoicesStreamService:
51
60
  *,
52
61
  max_tokens: int = 4096, # FIXME: ChatOption
53
62
  ) -> ChatChoicesStreamResponse:
54
- messages = []
55
- system: str | None = None
56
- for i, m in enumerate(request.v):
57
- if isinstance(m, SystemMessage):
58
- if i != 0 or system is not None:
59
- raise Exception('Only supports one system message and must be first')
60
- system = AnthropicChatChoicesService._get_msg_content(m) # noqa
61
- else:
62
- messages.append(dict(
63
- role=AnthropicChatChoicesService.ROLES_MAP[type(m)], # noqa
64
- content=check.isinstance(AnthropicChatChoicesService._get_msg_content(m), str), # noqa
65
- ))
66
-
67
- raw_request = dict(
63
+ messages, system = build_protocol_chat_messages(request.v)
64
+
65
+ tools: list[pt.ToolSpec] = []
66
+ with tv.TypedValues(*request.options).consume() as oc:
67
+ t: Tool
68
+ for t in oc.pop(Tool, []):
69
+ tools.append(build_protocol_tool(t))
70
+
71
+ a_req = pt.MessagesRequest(
68
72
  model=MODEL_NAMES.resolve(self._model_name.v),
69
- **lang.opt_kw(system=system),
73
+ system=system,
70
74
  messages=messages,
75
+ tools=tools or None,
71
76
  max_tokens=max_tokens,
72
77
  stream=True,
73
78
  )
74
79
 
80
+ raw_request = msh.marshal(a_req)
81
+
75
82
  http_request = http.HttpRequest(
76
83
  'https://api.anthropic.com/v1/messages',
77
84
  headers={
@@ -83,8 +90,8 @@ class AnthropicChatChoicesStreamService:
83
90
  )
84
91
 
85
92
  async with UseResources.or_new(request.options) as rs:
86
- http_client = rs.enter_context(http.client())
87
- http_response = rs.enter_context(http_client.stream_request(http_request))
93
+ http_client = await rs.enter_async_context(http.manage_async_client(self._http_client))
94
+ http_response = await rs.enter_async_context(await http_client.stream_request(http_request))
88
95
 
89
96
  async def inner(sink: StreamResponseSink[AiChoicesDeltas]) -> ta.Sequence[ChatChoicesOutputs] | None:
90
97
  msg_start: AnthropicSseDecoderEvents.MessageStart | None = None
@@ -94,8 +101,7 @@ class AnthropicChatChoicesStreamService:
94
101
  db = DelimitingBuffer([b'\r', b'\n', b'\r\n'])
95
102
  sd = sse.SseDecoder()
96
103
  while True:
97
- # FIXME: read1 not on response stream protocol
98
- b = http_response.stream.read1(self.READ_CHUNK_SIZE) # type: ignore[attr-defined]
104
+ b = await http_response.stream.read1(self.READ_CHUNK_SIZE)
99
105
  for l in db.feed(b):
100
106
  if isinstance(l, DelimitingBuffer.Incomplete):
101
107
  # FIXME: handle
@@ -123,19 +129,35 @@ class AnthropicChatChoicesStreamService:
123
129
  check.not_none(msg_start)
124
130
  check.none(cbk_start)
125
131
  cbk_start = ae
132
+
126
133
  if isinstance(ae.content_block, AnthropicSseDecoderEvents.ContentBlockStart.Text): # noqa
127
- await sink.emit(AiChoicesDeltas([AiChoiceDeltas([ContentAiChoiceDelta(
134
+ await sink.emit(AiChoicesDeltas([AiChoiceDeltas([ContentAiDelta(
128
135
  ae.content_block.text,
129
136
  )])]))
137
+
138
+ elif isinstance(ae.content_block, AnthropicSseDecoderEvents.ContentBlockStart.ToolUse): # noqa
139
+ await sink.emit(AiChoicesDeltas([AiChoiceDeltas([PartialToolUseAiDelta( # noqa
140
+ id=ae.content_block.id,
141
+ name=ae.content_block.name,
142
+ raw_args=ae.content_block.input,
143
+ )])]))
144
+
130
145
  else:
131
146
  raise TypeError(ae.content_block)
132
147
 
133
148
  case AnthropicSseDecoderEvents.ContentBlockDelta():
134
149
  check.not_none(cbk_start)
150
+
135
151
  if isinstance(ae.delta, AnthropicSseDecoderEvents.ContentBlockDelta.TextDelta):
136
- await sink.emit(AiChoicesDeltas([AiChoiceDeltas([ContentAiChoiceDelta(
152
+ await sink.emit(AiChoicesDeltas([AiChoiceDeltas([ContentAiDelta(
137
153
  ae.delta.text,
138
154
  )])]))
155
+
156
+ elif isinstance(ae.delta, AnthropicSseDecoderEvents.ContentBlockDelta.InputJsonDelta): # noqa
157
+ await sink.emit(AiChoicesDeltas([AiChoiceDeltas([PartialToolUseAiDelta( # noqa
158
+ raw_args=ae.delta.partial_json,
159
+ )])]))
160
+
139
161
  else:
140
162
  raise TypeError(ae.delta)
141
163
 
@@ -1,4 +1,4 @@
1
- import ddgs
1
+ from omlish import lang
2
2
 
3
3
  from ....search import SearchHit
4
4
  from ....search import SearchHits
@@ -7,6 +7,10 @@ from ....search import SearchResponse
7
7
  from ....search import static_check_is_search_service
8
8
 
9
9
 
10
+ with lang.auto_proxy_import(globals()):
11
+ import ddgs
12
+
13
+
10
14
  ##
11
15
 
12
16
 
File without changes
@@ -0,0 +1,69 @@
1
+ # ruff: noqa: PERF402
2
+ import typing as ta
3
+
4
+ from omlish.text.lorem import LOREM
5
+
6
+ from ....chat.choices.services import ChatChoicesRequest
7
+ from ....chat.choices.services import ChatChoicesResponse
8
+ from ....chat.choices.services import static_check_is_chat_choices_service
9
+ from ....chat.choices.stream.services import ChatChoicesStreamRequest
10
+ from ....chat.choices.stream.services import ChatChoicesStreamResponse
11
+ from ....chat.choices.stream.services import static_check_is_chat_choices_stream_service
12
+ from ....chat.choices.stream.types import AiChoiceDeltas
13
+ from ....chat.choices.stream.types import AiChoicesDeltas
14
+ from ....chat.choices.types import AiChoice
15
+ from ....chat.choices.types import ChatChoicesOutputs
16
+ from ....chat.messages import AiMessage
17
+ from ....chat.stream.types import ContentAiDelta
18
+ from ....resources import UseResources
19
+ from ....stream.services import StreamResponseSink
20
+ from ....stream.services import new_stream_response
21
+
22
+
23
+ ##
24
+
25
+
26
+ # @omlish-manifest $.minichain.registries.manifests.RegistryManifest(
27
+ # name='dummy',
28
+ # type='ChatChoicesService',
29
+ # )
30
+ @static_check_is_chat_choices_service
31
+ class DummyChatChoicesService:
32
+ async def invoke(self, request: ChatChoicesRequest) -> ChatChoicesResponse:
33
+ return ChatChoicesResponse([AiChoice([AiMessage(LOREM)])])
34
+
35
+
36
+ ##
37
+
38
+
39
+ # @omlish-manifest $.minichain.registries.manifests.RegistryManifest(
40
+ # name='dummy',
41
+ # type='ChatChoicesStreamService',
42
+ # )
43
+ @static_check_is_chat_choices_stream_service
44
+ class DummyChatChoicesStreamService:
45
+ async def invoke(self, request: ChatChoicesStreamRequest) -> ChatChoicesStreamResponse:
46
+ async with UseResources.or_new(request.options) as rs:
47
+ async def inner(sink: StreamResponseSink[AiChoicesDeltas]) -> ta.Sequence[ChatChoicesOutputs]:
48
+ for s in LOREM:
49
+ await sink.emit(AiChoicesDeltas([
50
+ AiChoiceDeltas([
51
+ ContentAiDelta(s),
52
+ ]),
53
+ ]))
54
+
55
+ return []
56
+
57
+ return await new_stream_response(rs, inner)
58
+
59
+
60
+ ##
61
+
62
+
63
+ # @omlish-manifest $.minichain.backends.strings.manifests.BackendStringsManifest(
64
+ # [
65
+ # 'ChatChoicesService',
66
+ # 'ChatChoicesStreamService',
67
+ # ],
68
+ # 'dummy',
69
+ # )
@@ -40,9 +40,15 @@ from .tools import build_tool_spec_schema
40
40
  class GoogleChatChoicesService:
41
41
  DEFAULT_MODEL_NAME: ta.ClassVar[ModelName] = ModelName(check.not_none(MODEL_NAMES.default))
42
42
 
43
- def __init__(self, *configs: ApiKey | ModelName) -> None:
43
+ def __init__(
44
+ self,
45
+ *configs: ApiKey | ModelName,
46
+ http_client: http.AsyncHttpClient | None = None,
47
+ ) -> None:
44
48
  super().__init__()
45
49
 
50
+ self._http_client = http_client
51
+
46
52
  with tv.consume(*configs) as cc:
47
53
  self._model_name = cc.pop(self.DEFAULT_MODEL_NAME)
48
54
  self._api_key = ApiKey.pop_secret(cc, env='GEMINI_API_KEY')
@@ -149,11 +155,12 @@ class GoogleChatChoicesService:
149
155
 
150
156
  model_name = MODEL_NAMES.resolve(self._model_name.v)
151
157
 
152
- resp = http.request(
158
+ resp = await http.async_request(
153
159
  f'{self.BASE_URL.rstrip("/")}/{model_name}:generateContent?key={key}',
154
160
  headers={'Content-Type': 'application/json'},
155
161
  data=json.dumps_compact(req_dct).encode('utf-8'),
156
162
  method='POST',
163
+ client=self._http_client,
157
164
  )
158
165
 
159
166
  resp_dct = json.loads(check.not_none(resp.data).decode('utf-8'))