Coverage for astrocyte/providers/composite.py: 100%
24 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-10-29 22:10 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-10-29 22:10 +0000
1"""Composite LLMProvider — split completion and embedding across two backends.
3Delegates ``complete()`` to one provider and ``embed()`` to another. The
4motivating composition is Claude-CLI completions (subscription auth, no
5embeddings surface) + local sentence-transformers embeddings::
7 from astrocyte.providers.claude_cli import ClaudeCliProvider
8 from astrocyte.providers.composite import CompositeLLMProvider
9 from astrocyte.providers.local_embeddings import LocalEmbeddingsProvider
11 provider = CompositeLLMProvider(
12 completion_provider=ClaudeCliProvider(model="haiku"),
13 embedding_provider=LocalEmbeddingsProvider(),
14 )
16Any pair of LLMProvider implementations composes the same way.
17"""
19from __future__ import annotations
21from typing import Any, ClassVar
23from astrocyte.types import Completion, LLMCapabilities, Message, ToolDefinition
26class CompositeLLMProvider:
27 """LLMProvider that routes complete() and embed() to different backends."""
29 SPI_VERSION: ClassVar[int] = 1
31 def __init__(
32 self,
33 *,
34 completion_provider: Any,
35 embedding_provider: Any,
36 ) -> None:
37 self._completion = completion_provider
38 self._embedding = embedding_provider
40 def capabilities(self) -> LLMCapabilities:
41 comp = self._completion.capabilities()
42 emb = self._embedding.capabilities()
43 return LLMCapabilities(
44 supports_multimodal_completion=comp.supports_multimodal_completion,
45 modalities_supported=comp.modalities_supported,
46 supports_multimodal_embedding=emb.supports_multimodal_embedding,
47 supports_batch_embed=emb.supports_batch_embed,
48 )
50 async def complete(
51 self,
52 messages: list[Message],
53 model: str | None = None,
54 max_tokens: int = 1024,
55 temperature: float = 0.0,
56 tools: list[ToolDefinition] | None = None,
57 tool_choice: str | None = None,
58 response_format: dict | None = None,
59 ) -> Completion:
60 return await self._completion.complete(
61 messages,
62 model=model,
63 max_tokens=max_tokens,
64 temperature=temperature,
65 tools=tools,
66 tool_choice=tool_choice,
67 response_format=response_format,
68 )
70 async def embed(
71 self,
72 texts: list[str],
73 model: str | None = None,
74 ) -> list[list[float]]:
75 return await self._embedding.embed(texts, model=model)
77 async def close(self) -> None:
78 for backend in (self._completion, self._embedding):
79 close = getattr(backend, "close", None)
80 if close is not None:
81 try:
82 await close()
83 except Exception: # noqa: BLE001 — best-effort teardown
84 pass