mmsp 0.5.0__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 (42) hide show
  1. mmsp/__init__.py +45 -0
  2. mmsp/abort_signal.py +135 -0
  3. mmsp/ant_messages/__init__.py +18 -0
  4. mmsp/ant_messages/client.py +395 -0
  5. mmsp/anthropic_official/__init__.py +18 -0
  6. mmsp/anthropic_official/client.py +491 -0
  7. mmsp/auto_client.py +281 -0
  8. mmsp/base_client.py +378 -0
  9. mmsp/deepseek_official/__init__.py +18 -0
  10. mmsp/deepseek_official/client.py +384 -0
  11. mmsp/errors.py +123 -0
  12. mmsp/gemini_generate_content/__init__.py +18 -0
  13. mmsp/gemini_generate_content/client.py +668 -0
  14. mmsp/gemini_official/__init__.py +18 -0
  15. mmsp/gemini_official/client.py +674 -0
  16. mmsp/integration/__init__.py +14 -0
  17. mmsp/integration/playground.py +3306 -0
  18. mmsp/integration/tracer.py +1812 -0
  19. mmsp/legacy.py +78 -0
  20. mmsp/minimax_official/__init__.py +18 -0
  21. mmsp/minimax_official/client.py +323 -0
  22. mmsp/moonshot_official/__init__.py +18 -0
  23. mmsp/moonshot_official/client.py +412 -0
  24. mmsp/openai_chat/__init__.py +18 -0
  25. mmsp/openai_chat/client.py +379 -0
  26. mmsp/openai_chat_vllm_adapter/__init__.py +4 -0
  27. mmsp/openai_chat_vllm_adapter/client.py +122 -0
  28. mmsp/openai_embedding/__init__.py +18 -0
  29. mmsp/openai_embedding/client.py +102 -0
  30. mmsp/openai_official/__init__.py +18 -0
  31. mmsp/openai_official/client.py +426 -0
  32. mmsp/openai_responses/__init__.py +18 -0
  33. mmsp/openai_responses/client.py +411 -0
  34. mmsp/registry.py +846 -0
  35. mmsp/stream_items.py +199 -0
  36. mmsp/types.py +258 -0
  37. mmsp/utils.py +305 -0
  38. mmsp/zai_official/__init__.py +18 -0
  39. mmsp/zai_official/client.py +406 -0
  40. mmsp-0.5.0.dist-info/METADATA +356 -0
  41. mmsp-0.5.0.dist-info/RECORD +42 -0
  42. mmsp-0.5.0.dist-info/WHEEL +4 -0
mmsp/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ # Copyright 2025 Prism Shadow. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .auto_client import AutoLLMClient
16
+ from .errors import (
17
+ EmptyResponseError,
18
+ MMSPError,
19
+ StreamProtocolError,
20
+ ToolCallArgumentParseError,
21
+ UnsupportedOperationError,
22
+ UnsupportedParameterError,
23
+ )
24
+ from .legacy import normalize_legacy_messages
25
+ from .registry import Currency, Modality, ModelPricing, SupportedModel, list_supported_models
26
+ from .types import PromptCaching, ThinkingLevel
27
+
28
+
29
+ __all__ = [
30
+ "AutoLLMClient",
31
+ "Currency",
32
+ "EmptyResponseError",
33
+ "MMSPError",
34
+ "Modality",
35
+ "ModelPricing",
36
+ "PromptCaching",
37
+ "StreamProtocolError",
38
+ "SupportedModel",
39
+ "ThinkingLevel",
40
+ "ToolCallArgumentParseError",
41
+ "UnsupportedOperationError",
42
+ "UnsupportedParameterError",
43
+ "list_supported_models",
44
+ "normalize_legacy_messages",
45
+ ]
mmsp/abort_signal.py ADDED
@@ -0,0 +1,135 @@
1
+ # Copyright 2025 Prism Shadow. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import asyncio
16
+ import threading
17
+ from contextlib import suppress
18
+ from typing import Any, Awaitable, TypeVar
19
+
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ class AbortSignal:
25
+ """Abort signal that can also trigger its own aborted state."""
26
+
27
+ def __init__(self) -> None:
28
+ self._lock = threading.Lock()
29
+ self._aborted = False
30
+ self._reason: Any = None
31
+ self._waiters: set[asyncio.Future[None]] = set()
32
+
33
+ @property
34
+ def aborted(self) -> bool:
35
+ with self._lock:
36
+ return self._aborted
37
+
38
+ @property
39
+ def reason(self) -> Any:
40
+ with self._lock:
41
+ return self._reason
42
+
43
+ def abort(self, reason: Any = None) -> None:
44
+ with self._lock:
45
+ if self._aborted:
46
+ return
47
+
48
+ self._aborted = True
49
+ self._reason = reason
50
+ waiters = tuple(self._waiters)
51
+ self._waiters.clear()
52
+
53
+ for waiter in waiters:
54
+ _notify_waiter(waiter)
55
+
56
+ async def wait(self) -> None:
57
+ loop = asyncio.get_running_loop()
58
+ waiter = loop.create_future()
59
+ with self._lock:
60
+ if self._aborted:
61
+ return
62
+
63
+ self._waiters.add(waiter)
64
+
65
+ try:
66
+ await waiter
67
+ finally:
68
+ with self._lock:
69
+ self._waiters.discard(waiter)
70
+
71
+ def throw_if_aborted(self) -> None:
72
+ with self._lock:
73
+ aborted = self._aborted
74
+ reason = self._reason
75
+
76
+ if aborted:
77
+ raise _cancelled_error(reason)
78
+
79
+
80
+ async def run_with_abort(awaitable: Awaitable[T], signal: AbortSignal) -> T:
81
+ """Run an awaitable and cancel it when the signal is aborted."""
82
+
83
+ task = asyncio.ensure_future(awaitable)
84
+
85
+ if signal.aborted:
86
+ task.cancel(signal.reason)
87
+ with suppress(asyncio.CancelledError):
88
+ await task
89
+ raise _cancelled_error(signal.reason)
90
+
91
+ abort_task = asyncio.create_task(signal.wait())
92
+
93
+ try:
94
+ done, _ = await asyncio.wait((task, abort_task), return_when=asyncio.FIRST_COMPLETED)
95
+ if task in done:
96
+ return await task
97
+
98
+ task.cancel(signal.reason)
99
+ with suppress(asyncio.CancelledError):
100
+ await task
101
+ raise _cancelled_error(signal.reason)
102
+ except asyncio.CancelledError:
103
+ if not task.done():
104
+ task.cancel()
105
+ with suppress(asyncio.CancelledError):
106
+ await task
107
+ raise
108
+ finally:
109
+ if not abort_task.done():
110
+ abort_task.cancel()
111
+ with suppress(asyncio.CancelledError):
112
+ await abort_task
113
+
114
+
115
+ def _set_waiter_result(waiter: asyncio.Future[None]) -> None:
116
+ if not waiter.done():
117
+ waiter.set_result(None)
118
+
119
+
120
+ def _notify_waiter(waiter: asyncio.Future[None]) -> None:
121
+ loop = waiter.get_loop()
122
+ if loop.is_closed():
123
+ return
124
+
125
+ if loop.is_running():
126
+ loop.call_soon_threadsafe(_set_waiter_result, waiter)
127
+ else:
128
+ _set_waiter_result(waiter)
129
+
130
+
131
+ def _cancelled_error(reason: Any) -> asyncio.CancelledError:
132
+ if reason is None:
133
+ return asyncio.CancelledError()
134
+
135
+ return asyncio.CancelledError(reason)
@@ -0,0 +1,18 @@
1
+ # Copyright 2025 Prism Shadow. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .client import AntMessagesClient
16
+
17
+
18
+ __all__ = ["AntMessagesClient"]
@@ -0,0 +1,395 @@
1
+ # Copyright 2025 Prism Shadow. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import re
16
+ from typing import Any, AsyncIterator
17
+
18
+ from anthropic import AsyncAnthropic
19
+ from anthropic.types.beta import BetaMessageParam, BetaRawMessageStreamEvent
20
+
21
+ from ..base_client import LLMClient
22
+ from ..errors import UnsupportedParameterError
23
+ from ..types import (
24
+ EventContentItem,
25
+ EventType,
26
+ FinishReason,
27
+ PromptCaching,
28
+ ThinkingLevel,
29
+ ToolChoice,
30
+ UniConfig,
31
+ UniEvent,
32
+ UniMessage,
33
+ UsageMetadata,
34
+ )
35
+ from ..utils import fix_openrouter_usage_metadata, is_debug_enabled, resolve_credentials
36
+
37
+
38
+ REDACTED_THINKING = "_REDACTED_THINKING"
39
+
40
+
41
+ class AntMessagesClient(LLMClient):
42
+ """Anthropic Messages-compatible client implementation."""
43
+
44
+ def __init__(
45
+ self,
46
+ model: str,
47
+ api_key: str | None = None,
48
+ base_url: str | None = None,
49
+ default_headers: dict[str, str] | None = None,
50
+ ):
51
+ """Initialize Anthropic Messages-compatible client with model, API key, and base URL."""
52
+ self._model = model
53
+ api_key, base_url = resolve_credentials(
54
+ self.__class__.__name__, api_key, base_url, "ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"
55
+ )
56
+ # send the credential through both header conventions: Anthropic and DeepSeek read
57
+ # x-api-key while gateways such as OpenRouter and Z.AI read Authorization: Bearer
58
+ self._client = AsyncAnthropic(
59
+ api_key=api_key, auth_token=api_key, base_url=base_url, default_headers=default_headers
60
+ )
61
+ # With no credential the SDK reads the None auth_token as unset and may fill it from
62
+ # ANTHROPIC_AUTH_TOKEN, so pin the token to the key it was given.
63
+ self._client.auth_token = api_key
64
+ self._history: list[UniMessage] = []
65
+
66
+ def _convert_image_url_to_source(self, url: str) -> dict[str, Any]:
67
+ """Convert image URL to an Anthropic image source block."""
68
+ if url.startswith("data:"):
69
+ match = re.match(r"data:([^;]+);base64,(.+)", url)
70
+ if not match:
71
+ raise ValueError(f"Invalid base64 image: {url}")
72
+
73
+ return {
74
+ "type": "image",
75
+ "source": {"type": "base64", "media_type": match.group(1), "data": match.group(2)},
76
+ }
77
+
78
+ return {"type": "image", "source": {"type": "url", "url": url}}
79
+
80
+ def _convert_thinking_level_to_thinking_config(self, thinking_level: ThinkingLevel) -> dict[str, Any]:
81
+ """Convert ThinkingLevel enum to the Messages API thinking config."""
82
+ # NONE is explicit rather than omitted because some servers (e.g. Z.AI) think by default
83
+ mapping = {
84
+ ThinkingLevel.NONE: {"thinking": {"type": "disabled"}},
85
+ ThinkingLevel.LOW: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "low"}},
86
+ ThinkingLevel.MEDIUM: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "medium"}},
87
+ ThinkingLevel.HIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}},
88
+ ThinkingLevel.XHIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "xhigh"}},
89
+ ThinkingLevel.MAX: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "max"}},
90
+ }
91
+ return mapping.get(thinking_level)
92
+
93
+ def _convert_tool_choice(self, tool_choice: ToolChoice) -> dict[str, str]:
94
+ """Convert ToolChoice to the Messages API tool_choice format."""
95
+ if isinstance(tool_choice, list):
96
+ if len(tool_choice) > 1:
97
+ raise UnsupportedParameterError(
98
+ self.__class__.__name__, "tool_choice", "The Messages API does not support multiple tool choices."
99
+ )
100
+
101
+ return {"type": "tool", "name": tool_choice[0]}
102
+ elif tool_choice == "none":
103
+ return {"type": "none"}
104
+ elif tool_choice == "auto":
105
+ return {"type": "auto"}
106
+ elif tool_choice == "required":
107
+ return {"type": "any"}
108
+
109
+ def transform_uni_config_to_model_config(self, config: UniConfig) -> dict[str, Any]:
110
+ """
111
+ Transform universal configuration to Anthropic Messages-compatible configuration.
112
+
113
+ Args:
114
+ config: Universal configuration dict
115
+
116
+ Returns:
117
+ Anthropic Messages API configuration dictionary
118
+ """
119
+ ant_config = {"model": self._model, "stream": True}
120
+
121
+ if config.get("system_prompt") is not None:
122
+ ant_config["system"] = config["system_prompt"]
123
+
124
+ if config.get("max_tokens") is not None:
125
+ ant_config["max_tokens"] = config["max_tokens"]
126
+ else:
127
+ ant_config["max_tokens"] = 64000 # the Messages API requires max_tokens to be specified
128
+
129
+ if config.get("temperature") is not None:
130
+ ant_config["temperature"] = config["temperature"]
131
+
132
+ if config.get("thinking_level") is not None:
133
+ ant_config.update(self._convert_thinking_level_to_thinking_config(config["thinking_level"]))
134
+
135
+ if config.get("thinking_summary") is not None:
136
+ # display lives on the thinking block, so a summary asked for on its own selects
137
+ # adaptive thinking. A disabled block is the one place it cannot ride along --
138
+ # "thinking.disabled.display: Extra inputs are not permitted" (400, verified live
139
+ # 2026-09-03) -- and thinking_level NONE disables thinking, leaving nothing to show.
140
+ thinking = ant_config.setdefault("thinking", {"type": "adaptive"})
141
+ if thinking["type"] != "disabled":
142
+ thinking["display"] = "summarized" if config["thinking_summary"] else "omitted"
143
+
144
+ # Convert tools to the Messages API tool schema
145
+ if config.get("tools") is not None:
146
+ ant_tools = []
147
+ for tool in config["tools"]:
148
+ ant_tool = {}
149
+ for key, value in tool.items():
150
+ ant_tool[key.replace("parameters", "input_schema")] = value
151
+
152
+ ant_tools.append(ant_tool)
153
+
154
+ ant_config["tools"] = ant_tools
155
+
156
+ # Convert tool_choice
157
+ if config.get("tool_choice") is not None:
158
+ ant_config["tool_choice"] = self._convert_tool_choice(config["tool_choice"])
159
+
160
+ if config.get("fast_mode"):
161
+ ant_config["speed"] = "fast"
162
+ ant_config["betas"] = ["fast-mode-2026-02-01"]
163
+
164
+ if config.get("prompt_caching") is not None and config["prompt_caching"] != PromptCaching.ENABLE:
165
+ raise UnsupportedParameterError(
166
+ self.__class__.__name__, "prompt_caching", "prompt_caching must be ENABLE for the Messages API."
167
+ )
168
+
169
+ return ant_config
170
+
171
+ def transform_uni_message_to_model_input(self, messages: list[UniMessage]) -> list[BetaMessageParam]:
172
+ """
173
+ Transform universal message format to the Messages API BetaMessageParam format.
174
+
175
+ Args:
176
+ messages: List of universal message dictionaries
177
+
178
+ Returns:
179
+ List of Messages API BetaMessageParam objects
180
+ """
181
+ ant_messages: list[BetaMessageParam] = []
182
+
183
+ for msg in messages:
184
+ content_blocks = []
185
+ for item in msg["content_items"]:
186
+ if item["type"] == "text.done":
187
+ content_blocks.append({"type": "text", "text": item["text"]})
188
+ elif item["type"] == "image_url.done":
189
+ content_blocks.append(self._convert_image_url_to_source(item["image_url"]))
190
+ elif item["type"] == "thinking.done":
191
+ if item["thinking"] == REDACTED_THINKING:
192
+ content_blocks.append({"type": "redacted_thinking", "data": item["fidelity"]["signature"]})
193
+ else:
194
+ # third-party servers accept thinking without a signature, but the
195
+ # official API requires the one it emitted
196
+ thinking_block = {"type": "thinking", "thinking": item["thinking"]}
197
+ signature = (item.get("fidelity") or {}).get("signature")
198
+ if signature is not None:
199
+ thinking_block["signature"] = signature
200
+
201
+ content_blocks.append(thinking_block)
202
+ elif item["type"] == "tool_call.done":
203
+ content_blocks.append(
204
+ {
205
+ "type": "tool_use",
206
+ "id": item["tool_call_id"],
207
+ "name": item["name"],
208
+ "input": item["arguments"],
209
+ }
210
+ )
211
+ elif item["type"] == "tool_result.done":
212
+ if "tool_call_id" not in item:
213
+ raise ValueError("tool_call_id is required for tool result.")
214
+
215
+ tool_result = [{"type": "text", "text": item["text"]}]
216
+ if "images" in item:
217
+ for image_url in item["images"]:
218
+ tool_result.append(self._convert_image_url_to_source(image_url))
219
+
220
+ content_blocks.append(
221
+ {"type": "tool_result", "content": tool_result, "tool_use_id": item["tool_call_id"]}
222
+ )
223
+ else:
224
+ raise ValueError(f"Unknown item: {item}")
225
+
226
+ ant_messages.append({"role": msg["role"], "content": content_blocks})
227
+
228
+ return ant_messages
229
+
230
+ def transform_model_output_to_uni_event(self, model_output: BetaRawMessageStreamEvent) -> UniEvent:
231
+ """
232
+ Transform one Messages API stream event into a universal event, identifying items by content block index.
233
+
234
+ Args:
235
+ model_output: Messages API streaming event
236
+
237
+ Returns:
238
+ Universal event dictionary, an empty delta event when the wire event carries nothing universal
239
+ """
240
+ event_type: EventType = "delta"
241
+ content_items: list[EventContentItem] = []
242
+ usage_metadata: UsageMetadata | None = None
243
+ finish_reason: FinishReason | None = None
244
+
245
+ ant_event_type = model_output.type
246
+ if ant_event_type == "content_block_start":
247
+ item_id = str(model_output.index)
248
+ block = model_output.content_block
249
+ if block.type == "tool_use":
250
+ content_items.append(
251
+ {
252
+ "type": "tool_call.delta",
253
+ "name": block.name,
254
+ "arguments": "",
255
+ "tool_call_id": block.id,
256
+ "fidelity": {"item_id": item_id},
257
+ }
258
+ )
259
+ elif block.type == "redacted_thinking":
260
+ content_items.append(
261
+ {
262
+ "type": "thinking.delta",
263
+ "thinking": REDACTED_THINKING,
264
+ "fidelity": {"item_id": item_id, "signature": block.data},
265
+ }
266
+ )
267
+
268
+ elif ant_event_type == "content_block_delta":
269
+ item_id = str(model_output.index)
270
+ delta = model_output.delta
271
+ if delta.type == "thinking_delta":
272
+ content_items.append(
273
+ {"type": "thinking.delta", "thinking": delta.thinking, "fidelity": {"item_id": item_id}}
274
+ )
275
+ elif delta.type == "text_delta":
276
+ content_items.append({"type": "text.delta", "text": delta.text, "fidelity": {"item_id": item_id}})
277
+ elif delta.type == "input_json_delta":
278
+ content_items.append(
279
+ {
280
+ "type": "tool_call.delta",
281
+ "name": "",
282
+ "arguments": delta.partial_json,
283
+ "tool_call_id": "",
284
+ "fidelity": {"item_id": item_id},
285
+ }
286
+ )
287
+ elif delta.type == "signature_delta":
288
+ # the last delta of a thinking block: its signature
289
+ content_items.append(
290
+ {
291
+ "type": "thinking.delta",
292
+ "thinking": "",
293
+ "fidelity": {"item_id": item_id, "signature": delta.signature},
294
+ }
295
+ )
296
+
297
+ elif ant_event_type == "message_start":
298
+ event_type = "stop"
299
+ usage = getattr(model_output.message, "usage", None)
300
+ if usage:
301
+ cache_creation_tokens = usage.cache_creation_input_tokens or 0
302
+ usage_metadata = {
303
+ "cached_tokens": usage.cache_read_input_tokens,
304
+ "prompt_tokens": usage.input_tokens + cache_creation_tokens,
305
+ "thoughts_tokens": None,
306
+ "response_tokens": None,
307
+ }
308
+
309
+ elif ant_event_type == "message_delta":
310
+ event_type = "stop"
311
+ stop_reason_mapping = {
312
+ "end_turn": "stop",
313
+ "max_tokens": "length",
314
+ "stop_sequence": "stop",
315
+ "tool_use": "tool_call",
316
+ }
317
+ stop_reason = getattr(model_output.delta, "stop_reason", None)
318
+ if stop_reason:
319
+ finish_reason = stop_reason_mapping.get(stop_reason, "unknown")
320
+
321
+ usage = getattr(model_output, "usage", None)
322
+ if usage:
323
+ # gateways report zero usage in message_start and the full counts here, so the
324
+ # delta also carries the input-side fields (None on servers that omit them)
325
+ if usage.input_tokens is not None:
326
+ prompt_tokens = usage.input_tokens + (usage.cache_creation_input_tokens or 0)
327
+ else:
328
+ prompt_tokens = None
329
+
330
+ output_details = getattr(usage, "output_tokens_details", None)
331
+ thinking_tokens = getattr(output_details, "thinking_tokens", None) if output_details else None
332
+ usage_metadata = fix_openrouter_usage_metadata(
333
+ {
334
+ "cached_tokens": usage.cache_read_input_tokens,
335
+ "prompt_tokens": prompt_tokens,
336
+ "thoughts_tokens": thinking_tokens,
337
+ "response_tokens": usage.output_tokens - (thinking_tokens or 0),
338
+ },
339
+ str(self._client.base_url),
340
+ )
341
+
342
+ elif ant_event_type in [
343
+ "content_block_stop",
344
+ "message_stop",
345
+ "text",
346
+ "thinking",
347
+ "signature",
348
+ "input_json",
349
+ "ping",
350
+ ]:
351
+ # a block needs no stop: it is done when the next one begins or the stream ends. The SDK
352
+ # drops the "ping" heartbeat at the SSE layer; it reaches here only from gateways that
353
+ # relabel it onto another event
354
+ pass
355
+
356
+ elif is_debug_enabled():
357
+ raise ValueError(f"Unknown output: {model_output}")
358
+
359
+ else:
360
+ # a gateway injects its own events (heartbeats, cost tickers) into the stream, and
361
+ # killing a long generation over one costs more than dropping it
362
+ pass
363
+
364
+ return {
365
+ "role": "assistant",
366
+ "event_type": event_type,
367
+ "content_items": content_items,
368
+ "usage_metadata": usage_metadata,
369
+ "finish_reason": finish_reason,
370
+ }
371
+
372
+ async def _streaming_response_internal(
373
+ self,
374
+ messages: list[UniMessage],
375
+ config: UniConfig,
376
+ ) -> AsyncIterator[UniEvent]:
377
+ """Stream generate using an Anthropic Messages-compatible API with unified conversion methods."""
378
+ # Use unified config conversion
379
+ ant_config = self.transform_uni_config_to_model_config(config)
380
+
381
+ # Use unified message conversion
382
+ ant_messages = self.transform_uni_message_to_model_input(messages)
383
+
384
+ stream = await self._client.beta.messages.create(**ant_config, messages=ant_messages)
385
+ async for event in stream:
386
+ yield self.transform_model_output_to_uni_event(event)
387
+
388
+ async def list_models(self) -> list[str]:
389
+ """
390
+ List the model ids the configured endpoint serves.
391
+
392
+ Returns:
393
+ list[str]: The model ids, in the order the endpoint returned them.
394
+ """
395
+ return [model.id async for model in self._client.models.list()]
@@ -0,0 +1,18 @@
1
+ # Copyright 2025 Prism Shadow. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .client import AnthropicOfficialClient
16
+
17
+
18
+ __all__ = ["AnthropicOfficialClient"]