ommlds 0.0.0.dev502__py3-none-any.whl → 0.0.0.dev504__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 (39) hide show
  1. ommlds/.omlish-manifests.json +14 -7
  2. ommlds/backends/anthropic/protocol/_dataclasses.py +16 -16
  3. ommlds/backends/cerebras/_dataclasses.py +42 -42
  4. ommlds/backends/google/protocol/_dataclasses.py +64 -64
  5. ommlds/backends/groq/_dataclasses.py +36 -36
  6. ommlds/backends/ollama/_dataclasses.py +28 -28
  7. ommlds/backends/openai/protocol/_dataclasses.py +88 -88
  8. ommlds/backends/tavily/_dataclasses.py +16 -16
  9. ommlds/cli/_dataclasses.py +64 -114
  10. ommlds/cli/backends/inject.py +20 -0
  11. ommlds/cli/backends/meta.py +47 -0
  12. ommlds/cli/sessions/chat/drivers/ai/tools.py +3 -7
  13. ommlds/cli/sessions/chat/facades/commands/base.py +1 -1
  14. ommlds/minichain/__init__.py +38 -4
  15. ommlds/minichain/_dataclasses.py +452 -289
  16. ommlds/minichain/backends/impls/anthropic/stream.py +1 -1
  17. ommlds/minichain/backends/impls/cerebras/names.py +15 -0
  18. ommlds/minichain/backends/impls/cerebras/stream.py +39 -52
  19. ommlds/minichain/backends/impls/google/chat.py +11 -82
  20. ommlds/minichain/backends/impls/google/protocol.py +105 -0
  21. ommlds/minichain/backends/impls/google/stream.py +49 -132
  22. ommlds/minichain/backends/impls/groq/stream.py +40 -53
  23. ommlds/minichain/backends/impls/ollama/chat.py +1 -1
  24. ommlds/minichain/backends/impls/openai/format.py +1 -0
  25. ommlds/minichain/backends/impls/openai/stream.py +40 -55
  26. ommlds/minichain/http/__init__.py +0 -0
  27. ommlds/minichain/http/stream.py +195 -0
  28. ommlds/minichain/resources.py +22 -1
  29. ommlds/minichain/stream/services.py +24 -1
  30. ommlds/minichain/wrappers/firstinwins.py +1 -1
  31. ommlds/minichain/wrappers/instrument.py +1 -1
  32. ommlds/minichain/wrappers/retry.py +34 -13
  33. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/METADATA +4 -4
  34. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/RECORD +38 -36
  35. ommlds/minichain/stream/wrap.py +0 -62
  36. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/WHEEL +0 -0
  37. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/entry_points.txt +0 -0
  38. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/licenses/LICENSE +0 -0
  39. {ommlds-0.0.0.dev502.dist-info → ommlds-0.0.0.dev504.dist-info}/top_level.txt +0 -0
@@ -105,7 +105,7 @@ class AnthropicChatChoicesStreamService:
105
105
  for l in db.feed(b):
106
106
  if isinstance(l, DelimitingBuffer.Incomplete):
107
107
  # FIXME: handle
108
- return []
108
+ raise TypeError(l)
109
109
 
110
110
  # FIXME: https://docs.anthropic.com/en/docs/build-with-claude/streaming
111
111
  for so in sd.process_line(l):
@@ -12,9 +12,24 @@ MODEL_NAMES = ModelNameCollection(
12
12
  default='gpt-oss-120b',
13
13
  aliases={
14
14
  'llama3.1-8b': None,
15
+
15
16
  'llama-3.3-70b': None,
17
+ 'llama3': 'llama-3.3-70b',
18
+
16
19
  'gpt-oss-120b': None,
20
+ 'gpt-oss': 'gpt-oss-120b',
21
+
17
22
  'qwen-3-32b': None,
23
+ 'qwen3': 'qwen-3-32b',
24
+
25
+ ##
26
+ # preview
27
+
28
+ 'qwen-3-235b-a22b-instruct-2507': None,
29
+ 'qwen-3-235b': 'qwen-3-235b-a22b-instruct-2507',
30
+
31
+ 'zai-glm-4.7': None,
32
+ 'glm': 'zai-glm-4.7',
18
33
  },
19
34
  )
20
35
 
@@ -6,20 +6,17 @@ from omlish import typedvalues as tv
6
6
  from omlish.formats import json
7
7
  from omlish.http import all as http
8
8
  from omlish.http import sse
9
- from omlish.io.buffers import DelimitingBuffer
10
9
 
11
10
  from .....backends.cerebras import protocol as pt
12
- from ....chat.choices.services import ChatChoicesOutputs
13
11
  from ....chat.choices.stream.services import ChatChoicesStreamRequest
14
12
  from ....chat.choices.stream.services import ChatChoicesStreamResponse
15
13
  from ....chat.choices.stream.services import static_check_is_chat_choices_stream_service
16
14
  from ....chat.choices.stream.types import AiChoicesDeltas
17
15
  from ....chat.tools.types import Tool
18
16
  from ....configs import Config
19
- from ....resources import UseResources
17
+ from ....http.stream import BytesHttpStreamResponseBuilder
18
+ from ....http.stream import SimpleSseLinesHttpStreamResponseHandler
20
19
  from ....standard import ApiKey
21
- from ....stream.services import StreamResponseSink
22
- from ....stream.services import new_stream_response
23
20
  from .chat import CerebrasChatChoicesService
24
21
  from .names import MODEL_NAMES
25
22
  from .protocol import build_cer_request_messages
@@ -49,6 +46,35 @@ class CerebrasChatChoicesStreamService:
49
46
  self._model_name = cc.pop(CerebrasChatChoicesService.DEFAULT_MODEL_NAME)
50
47
  self._api_key = ApiKey.pop_secret(cc, env='CEREBRAS_API_KEY')
51
48
 
49
+ URL: ta.ClassVar[str] = 'https://api.cerebras.ai/v1/chat/completions'
50
+
51
+ def _process_sse(self, so: sse.SseDecoderOutput) -> ta.Sequence[AiChoicesDeltas | None]:
52
+ if not (isinstance(so, sse.SseEvent) and so.type == b'message'):
53
+ return []
54
+
55
+ ss = so.data.decode('utf-8')
56
+ if ss == '[DONE]':
57
+ return [None]
58
+
59
+ sj = json.loads(ss) # ChatCompletionChunk
60
+
61
+ check.state(sj['object'] == 'chat.completion.chunk')
62
+
63
+ ccc = msh.unmarshal(sj, pt.ChatCompletionChunk)
64
+
65
+ # FIXME: stop reason
66
+ if not ccc.choices:
67
+ return []
68
+
69
+ if any(choice.finish_reason for choice in ccc.choices):
70
+ check.state(all(choice.finish_reason for choice in ccc.choices))
71
+ return [None]
72
+
73
+ return [AiChoicesDeltas([
74
+ build_mc_ai_choice_deltas(choice.delta)
75
+ for choice in ccc.choices
76
+ ])]
77
+
52
78
  READ_CHUNK_SIZE: ta.ClassVar[int] = -1
53
79
 
54
80
  async def invoke(self, request: ChatChoicesStreamRequest) -> ChatChoicesStreamResponse:
@@ -76,50 +102,11 @@ class CerebrasChatChoicesStreamService:
76
102
  data=json.dumps(raw_request).encode('utf-8'),
77
103
  )
78
104
 
79
- async with UseResources.or_new(request.options) as rs:
80
- http_client = await rs.enter_async_context(http.manage_async_client(self._http_client))
81
- http_response = await rs.enter_async_context(await http_client.stream_request(http_request))
82
-
83
- async def inner(sink: StreamResponseSink[AiChoicesDeltas]) -> ta.Sequence[ChatChoicesOutputs]:
84
- db = DelimitingBuffer([b'\r', b'\n', b'\r\n'])
85
- sd = sse.SseDecoder()
86
- while True:
87
- b = await http_response.stream.read1(self.READ_CHUNK_SIZE)
88
- for l in db.feed(b):
89
- if isinstance(l, DelimitingBuffer.Incomplete):
90
- # FIXME: handle
91
- return []
92
-
93
- # FIXME: https://platform.openai.com/docs/guides/function-calling?api-mode=responses#streaming
94
- for so in sd.process_line(l):
95
- if isinstance(so, sse.SseEvent) and so.type == b'message':
96
- ss = so.data.decode('utf-8')
97
- if ss == '[DONE]':
98
- return []
99
-
100
- sj = json.loads(ss) # ChatCompletionChunk
101
-
102
- check.state(sj['object'] == 'chat.completion.chunk')
103
-
104
- ccc = msh.unmarshal(sj, pt.ChatCompletionChunk)
105
-
106
- # FIXME: stop reason
107
- if not ccc.choices:
108
- continue
109
-
110
- if any(choice.finish_reason for choice in ccc.choices):
111
- check.state(all(choice.finish_reason for choice in ccc.choices))
112
- break
113
-
114
- await sink.emit(AiChoicesDeltas([
115
- build_mc_ai_choice_deltas(choice.delta)
116
- for choice in ccc.choices
117
- ]))
118
-
119
- if not b:
120
- return []
121
-
122
- # raw_response = json.loads(check.not_none(http_response.data).decode('utf-8'))
123
- # return rh.build_response(raw_response)
124
-
125
- return await new_stream_response(rs, inner)
105
+ return await BytesHttpStreamResponseBuilder(
106
+ self._http_client,
107
+ lambda http_response: SimpleSseLinesHttpStreamResponseHandler(self._process_sse).as_lines().as_bytes(),
108
+ read_chunk_size=self.READ_CHUNK_SIZE,
109
+ ).new_stream_response(
110
+ http_request,
111
+ request.options,
112
+ )
@@ -16,16 +16,14 @@ from ....chat.choices.services import static_check_is_chat_choices_service
16
16
  from ....chat.choices.types import AiChoice
17
17
  from ....chat.messages import AiMessage
18
18
  from ....chat.messages import AnyAiMessage
19
- from ....chat.messages import Message
20
- from ....chat.messages import SystemMessage
21
19
  from ....chat.messages import ToolUseMessage
22
- from ....chat.messages import ToolUseResultMessage
23
- from ....chat.messages import UserMessage
24
20
  from ....chat.tools.types import Tool
25
21
  from ....models.configs import ModelName
26
22
  from ....standard import ApiKey
27
23
  from ....tools.types import ToolUse
28
24
  from .names import MODEL_NAMES
25
+ from .protocol import make_msg_content
26
+ from .protocol import pop_system_instructions
29
27
  from .tools import build_tool_spec_schema
30
28
 
31
29
 
@@ -53,90 +51,14 @@ class GoogleChatChoicesService:
53
51
  self._model_name = cc.pop(self.DEFAULT_MODEL_NAME)
54
52
  self._api_key = ApiKey.pop_secret(cc, env='GEMINI_API_KEY')
55
53
 
56
- def _get_msg_content(self, m: Message) -> str | None:
57
- if isinstance(m, AiMessage):
58
- return check.isinstance(m.c, str)
59
-
60
- elif isinstance(m, (SystemMessage, UserMessage)):
61
- return check.isinstance(m.c, str)
62
-
63
- else:
64
- raise TypeError(m)
65
-
66
54
  BASE_URL: ta.ClassVar[str] = 'https://generativelanguage.googleapis.com/v1beta/models'
67
55
 
68
- ROLES_MAP: ta.ClassVar[ta.Mapping[type[Message], str]] = {
69
- UserMessage: 'user',
70
- AiMessage: 'model',
71
- ToolUseMessage: 'model',
72
- }
73
-
74
56
  async def invoke(
75
57
  self,
76
58
  request: ChatChoicesRequest,
77
59
  ) -> ChatChoicesResponse:
78
60
  key = check.not_none(self._api_key).reveal()
79
61
 
80
- g_sys_content: pt.Content | None = None
81
- g_contents: list[pt.Content] = []
82
- for i, m in enumerate(request.v):
83
- if isinstance(m, SystemMessage):
84
- check.arg(i == 0)
85
- check.none(g_sys_content)
86
- g_sys_content = pt.Content(
87
- parts=[pt.Part(
88
- text=check.not_none(self._get_msg_content(m)),
89
- )],
90
- )
91
-
92
- elif isinstance(m, ToolUseResultMessage):
93
- tr_resp_val: pt.Value
94
- if m.tur.c is None:
95
- tr_resp_val = pt.NullValue() # type: ignore[unreachable]
96
- elif isinstance(m.tur.c, str):
97
- tr_resp_val = pt.StringValue(m.tur.c)
98
- else:
99
- raise TypeError(m.tur.c)
100
- g_contents.append(pt.Content(
101
- parts=[pt.Part(
102
- function_response=pt.FunctionResponse(
103
- id=m.tur.id,
104
- name=m.tur.name,
105
- response={
106
- 'value': tr_resp_val,
107
- },
108
- ),
109
- )],
110
- ))
111
-
112
- elif isinstance(m, AiMessage):
113
- g_contents.append(pt.Content(
114
- parts=[pt.Part(
115
- text=check.not_none(self._get_msg_content(m)),
116
- )],
117
- role='model',
118
- ))
119
-
120
- elif isinstance(m, ToolUseMessage):
121
- g_contents.append(pt.Content(
122
- parts=[pt.Part(
123
- function_call=pt.FunctionCall(
124
- id=m.tu.id,
125
- name=m.tu.name,
126
- args=m.tu.args,
127
- ),
128
- )],
129
- role='model',
130
- ))
131
-
132
- else:
133
- g_contents.append(pt.Content(
134
- parts=[pt.Part(
135
- text=check.not_none(self._get_msg_content(m)),
136
- )],
137
- role=self.ROLES_MAP[type(m)], # type: ignore[arg-type]
138
- ))
139
-
140
62
  g_tools: list[pt.Tool] = []
141
63
  with tv.TypedValues(*request.options).consume() as oc:
142
64
  t: Tool
@@ -145,10 +67,17 @@ class GoogleChatChoicesService:
145
67
  function_declarations=[build_tool_spec_schema(t.spec)],
146
68
  ))
147
69
 
70
+ msgs = list(request.v)
71
+
72
+ system_inst = pop_system_instructions(msgs)
73
+
148
74
  g_req = pt.GenerateContentRequest(
149
- contents=g_contents or None,
75
+ contents=[
76
+ make_msg_content(m)
77
+ for m in msgs
78
+ ] or None,
150
79
  tools=g_tools or None,
151
- system_instruction=g_sys_content,
80
+ system_instruction=system_inst,
152
81
  )
153
82
 
154
83
  req_dct = msh.marshal(g_req)
@@ -0,0 +1,105 @@
1
+ import typing as ta
2
+
3
+ from omlish import check
4
+
5
+ from .....backends.google.protocol import types as pt
6
+ from ....chat.messages import AiMessage
7
+ from ....chat.messages import Message
8
+ from ....chat.messages import SystemMessage
9
+ from ....chat.messages import ToolUseMessage
10
+ from ....chat.messages import ToolUseResultMessage
11
+ from ....chat.messages import UserMessage
12
+
13
+
14
+ ##
15
+
16
+
17
+ ROLES_MAP: ta.Mapping[type[Message], pt.ContentRole | None] = { # noqa
18
+ SystemMessage: None,
19
+ UserMessage: 'user',
20
+ AiMessage: 'model',
21
+ ToolUseMessage: 'model',
22
+ }
23
+
24
+
25
+ def make_str_content(
26
+ s: str | None,
27
+ *,
28
+ role: pt.ContentRole | None = None,
29
+ ) -> pt.Content | None:
30
+ if s is None:
31
+ return None
32
+
33
+ return pt.Content(
34
+ parts=[pt.Part(
35
+ text=check.not_none(s),
36
+ )],
37
+ role=role,
38
+ )
39
+
40
+
41
+ def make_msg_content(m: Message) -> pt.Content:
42
+ if isinstance(m, (AiMessage, SystemMessage, UserMessage)):
43
+ return check.not_none(make_str_content(
44
+ check.isinstance(m.c, str),
45
+ role=ROLES_MAP[type(m)],
46
+ ))
47
+
48
+ elif isinstance(m, ToolUseResultMessage):
49
+ tr_resp_val: pt.Value
50
+ if m.tur.c is None:
51
+ tr_resp_val = pt.NullValue() # type: ignore[unreachable]
52
+ elif isinstance(m.tur.c, str):
53
+ tr_resp_val = pt.StringValue(m.tur.c)
54
+ else:
55
+ raise TypeError(m.tur.c)
56
+
57
+ return pt.Content(
58
+ parts=[pt.Part(
59
+ function_response=pt.FunctionResponse(
60
+ id=m.tur.id,
61
+ name=m.tur.name,
62
+ response={
63
+ 'value': tr_resp_val,
64
+ },
65
+ ),
66
+ )],
67
+ )
68
+
69
+ elif isinstance(m, ToolUseMessage):
70
+ return pt.Content(
71
+ parts=[pt.Part(
72
+ function_call=pt.FunctionCall(
73
+ id=m.tu.id,
74
+ name=m.tu.name,
75
+ args=m.tu.args,
76
+ ),
77
+ )],
78
+ role='model',
79
+ )
80
+
81
+ else:
82
+ raise TypeError(m)
83
+
84
+
85
+ def pop_system_instructions(msgs: list[Message]) -> pt.Content | None:
86
+ if not msgs:
87
+ return None
88
+
89
+ m0 = msgs[0]
90
+ if not isinstance(m0 := msgs[0], SystemMessage):
91
+ return None
92
+
93
+ msgs.pop(0)
94
+ return make_msg_content(m0)
95
+
96
+
97
+ def get_msg_content(m: Message) -> str | None:
98
+ if isinstance(m, AiMessage):
99
+ return check.isinstance(m.c, str)
100
+
101
+ elif isinstance(m, (SystemMessage, UserMessage)):
102
+ return check.isinstance(m.c, str)
103
+
104
+ else:
105
+ raise TypeError(m)
@@ -8,7 +8,7 @@ from omlish import marshal as msh
8
8
  from omlish import typedvalues as tv
9
9
  from omlish.formats import json
10
10
  from omlish.http import all as http
11
- from omlish.io.buffers import DelimitingBuffer
11
+ from omlish.http import sse
12
12
 
13
13
  from .....backends.google.protocol import types as pt
14
14
  from ....chat.choices.stream.services import ChatChoicesStreamRequest
@@ -16,22 +16,16 @@ from ....chat.choices.stream.services import ChatChoicesStreamResponse
16
16
  from ....chat.choices.stream.services import static_check_is_chat_choices_stream_service
17
17
  from ....chat.choices.stream.types import AiChoiceDeltas
18
18
  from ....chat.choices.stream.types import AiChoicesDeltas
19
- from ....chat.choices.types import ChatChoicesOutputs
20
- from ....chat.messages import AiMessage
21
- from ....chat.messages import Message
22
- from ....chat.messages import SystemMessage
23
- from ....chat.messages import ToolUseMessage
24
- from ....chat.messages import ToolUseResultMessage
25
- from ....chat.messages import UserMessage
26
19
  from ....chat.stream.types import ContentAiDelta
27
20
  from ....chat.stream.types import ToolUseAiDelta
28
21
  from ....chat.tools.types import Tool
22
+ from ....http.stream import BytesHttpStreamResponseBuilder
23
+ from ....http.stream import SimpleSseLinesHttpStreamResponseHandler
29
24
  from ....models.configs import ModelName
30
- from ....resources import UseResources
31
25
  from ....standard import ApiKey
32
- from ....stream.services import StreamResponseSink
33
- from ....stream.services import new_stream_response
34
26
  from .names import MODEL_NAMES
27
+ from .protocol import make_msg_content
28
+ from .protocol import pop_system_instructions
35
29
  from .tools import build_tool_spec_schema
36
30
 
37
31
 
@@ -59,71 +53,38 @@ class GoogleChatChoicesStreamService:
59
53
  self._model_name = cc.pop(self.DEFAULT_MODEL_NAME)
60
54
  self._api_key = ApiKey.pop_secret(cc, env='GEMINI_API_KEY')
61
55
 
62
- def _make_str_content(
63
- self,
64
- s: str | None,
65
- *,
66
- role: pt.ContentRole | None = None,
67
- ) -> pt.Content | None:
68
- if s is None:
69
- return None
70
-
71
- return pt.Content(
72
- parts=[pt.Part(
73
- text=check.not_none(s),
74
- )],
75
- role=role,
76
- )
77
-
78
- def _make_msg_content(self, m: Message) -> pt.Content:
79
- if isinstance(m, (AiMessage, SystemMessage, UserMessage)):
80
- return check.not_none(self._make_str_content(
81
- check.isinstance(m.c, str),
82
- role=self.ROLES_MAP[type(m)],
83
- ))
84
-
85
- elif isinstance(m, ToolUseResultMessage):
86
- tr_resp_val: pt.Value
87
- if m.tur.c is None:
88
- tr_resp_val = pt.NullValue() # type: ignore[unreachable]
89
- elif isinstance(m.tur.c, str):
90
- tr_resp_val = pt.StringValue(m.tur.c)
91
- else:
92
- raise TypeError(m.tur.c)
93
- return pt.Content(
94
- parts=[pt.Part(
95
- function_response=pt.FunctionResponse(
96
- id=m.tur.id,
97
- name=m.tur.name,
98
- response={
99
- 'value': tr_resp_val,
100
- },
101
- ),
102
- )],
103
- )
104
-
105
- elif isinstance(m, ToolUseMessage):
106
- return pt.Content(
107
- parts=[pt.Part(
108
- function_call=pt.FunctionCall(
109
- id=m.tu.id,
110
- name=m.tu.name,
111
- args=m.tu.args,
112
- ),
113
- )],
114
- role='model',
115
- )
116
-
117
- else:
118
- raise TypeError(m)
119
-
120
56
  BASE_URL: ta.ClassVar[str] = 'https://generativelanguage.googleapis.com/v1beta/models'
121
57
 
122
- ROLES_MAP: ta.ClassVar[ta.Mapping[type[Message], pt.ContentRole | None]] = { # noqa
123
- SystemMessage: None,
124
- UserMessage: 'user',
125
- AiMessage: 'model',
126
- }
58
+ def _process_sse(self, so: sse.SseDecoderOutput) -> ta.Iterable[AiChoicesDeltas | None]:
59
+ if not (isinstance(so, sse.SseEvent) and so.type == b'message'):
60
+ return
61
+
62
+ gcr = msh.unmarshal(json.loads(so.data.decode('utf-8')), pt.GenerateContentResponse) # noqa
63
+ cnd = check.single(check.not_none(gcr.candidates))
64
+
65
+ for p in check.not_none(cnd.content).parts or []:
66
+ if (txt := p.text) is not None:
67
+ check.none(p.function_call)
68
+ yield AiChoicesDeltas([
69
+ AiChoiceDeltas([
70
+ ContentAiDelta(check.not_none(txt)),
71
+ ]),
72
+ ])
73
+
74
+ elif (fc := p.function_call) is not None:
75
+ check.none(p.text)
76
+ yield AiChoicesDeltas([
77
+ AiChoiceDeltas([
78
+ ToolUseAiDelta(
79
+ id=fc.id,
80
+ name=fc.name,
81
+ args=fc.args,
82
+ ),
83
+ ]),
84
+ ])
85
+
86
+ else:
87
+ raise ValueError(p)
127
88
 
128
89
  READ_CHUNK_SIZE: ta.ClassVar[int] = -1
129
90
 
@@ -133,13 +94,6 @@ class GoogleChatChoicesStreamService:
133
94
  ) -> ChatChoicesStreamResponse:
134
95
  key = check.not_none(self._api_key).reveal()
135
96
 
136
- msgs = list(request.v)
137
-
138
- system_inst: pt.Content | None = None
139
- if msgs and isinstance(m0 := msgs[0], SystemMessage):
140
- system_inst = self._make_msg_content(m0)
141
- msgs.pop(0)
142
-
143
97
  g_tools: list[pt.Tool] = []
144
98
  with tv.TypedValues(*request.options).consume() as oc:
145
99
  t: Tool
@@ -148,11 +102,15 @@ class GoogleChatChoicesStreamService:
148
102
  function_declarations=[build_tool_spec_schema(t.spec)],
149
103
  ))
150
104
 
105
+ msgs = list(request.v)
106
+
107
+ system_inst = pop_system_instructions(msgs)
108
+
151
109
  g_req = pt.GenerateContentRequest(
152
110
  contents=[
153
- self._make_msg_content(m)
111
+ make_msg_content(m)
154
112
  for m in msgs
155
- ],
113
+ ] or None,
156
114
  tools=g_tools or None,
157
115
  system_instruction=system_inst,
158
116
  )
@@ -168,52 +126,11 @@ class GoogleChatChoicesStreamService:
168
126
  method='POST',
169
127
  )
170
128
 
171
- async with UseResources.or_new(request.options) as rs:
172
- http_client = await rs.enter_async_context(http.manage_async_client(self._http_client))
173
- http_response = await rs.enter_async_context(await http_client.stream_request(http_request))
174
-
175
- async def inner(sink: StreamResponseSink[AiChoicesDeltas]) -> ta.Sequence[ChatChoicesOutputs] | None:
176
- db = DelimitingBuffer([b'\r', b'\n', b'\r\n'])
177
- while True:
178
- b = await http_response.stream.read1(self.READ_CHUNK_SIZE)
179
- for bl in db.feed(b):
180
- if isinstance(bl, DelimitingBuffer.Incomplete):
181
- # FIXME: handle
182
- return []
183
-
184
- l = bl.decode('utf-8')
185
- if not l:
186
- continue
187
-
188
- if l.startswith('data: '):
189
- gcr = msh.unmarshal(json.loads(l[6:]), pt.GenerateContentResponse) # noqa
190
- cnd = check.single(check.not_none(gcr.candidates))
191
-
192
- for p in check.not_none(cnd.content).parts or []:
193
- if (txt := p.text) is not None:
194
- check.none(p.function_call)
195
- await sink.emit(AiChoicesDeltas([
196
- AiChoiceDeltas([
197
- ContentAiDelta(check.not_none(txt)),
198
- ]),
199
- ]))
200
-
201
- elif (fc := p.function_call) is not None:
202
- check.none(p.text)
203
- await sink.emit(AiChoicesDeltas([
204
- AiChoiceDeltas([
205
- ToolUseAiDelta(
206
- id=fc.id,
207
- name=fc.name,
208
- args=fc.args,
209
- ),
210
- ]),
211
- ]))
212
-
213
- else:
214
- raise ValueError(p)
215
-
216
- if not b:
217
- return []
218
-
219
- return await new_stream_response(rs, inner)
129
+ return await BytesHttpStreamResponseBuilder(
130
+ self._http_client,
131
+ lambda http_response: SimpleSseLinesHttpStreamResponseHandler(self._process_sse).as_lines().as_bytes(),
132
+ read_chunk_size=self.READ_CHUNK_SIZE,
133
+ ).new_stream_response(
134
+ http_request,
135
+ request.options,
136
+ )