donkit-llm 0.1.7__tar.gz → 0.1.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: donkit-llm
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Unified LLM model implementations for Donkit (OpenAI, Azure OpenAI, Claude, Vertex AI, Ollama)
5
5
  License: MIT
6
6
  Author: Donkit AI
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "donkit-llm"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "Unified LLM model implementations for Donkit (OpenAI, Azure OpenAI, Claude, Vertex AI, Ollama)"
5
5
  authors = ["Donkit AI <opensource@donkit.ai>"]
6
6
  license = "MIT"
@@ -19,6 +19,7 @@ donkit-ragops-api-gateway-client = "^0.1.5"
19
19
  ruff = "^0.13.3"
20
20
  pytest = "^8.4.2"
21
21
  pytest-asyncio = "^1.3.0"
22
+ donkit-llm-gate-client = { path = "../llm-gate-client", develop = true }
22
23
 
23
24
  [build-system]
24
25
  requires = ["poetry-core>=1.9.0"]
@@ -26,6 +26,12 @@ from .factory import ModelFactory
26
26
  from .gemini_model import GeminiModel, GeminiEmbeddingModel
27
27
  from .donkit_model import DonkitModel
28
28
 
29
+
30
+ try:
31
+ from .llm_gate_model import LLMGateModel
32
+ except ModuleNotFoundError:
33
+ LLMGateModel = None
34
+
29
35
  __all__ = [
30
36
  "ModelFactory",
31
37
  # Abstract base
@@ -58,3 +64,6 @@ __all__ = [
58
64
  "GeminiEmbeddingModel",
59
65
  "DonkitModel",
60
66
  ]
67
+
68
+ if LLMGateModel is not None:
69
+ __all__.append("LLMGateModel")
@@ -4,6 +4,11 @@ from .claude_model import ClaudeModel
4
4
  from .claude_model import ClaudeVertexModel
5
5
  from .donkit_model import DonkitModel
6
6
  from .gemini_model import GeminiModel
7
+
8
+ try:
9
+ from .llm_gate_model import LLMGateModel
10
+ except ModuleNotFoundError:
11
+ LLMGateModel = None
7
12
  from .model_abstract import LLMModelAbstract
8
13
  from .openai_model import AzureOpenAIEmbeddingModel
9
14
  from .openai_model import AzureOpenAIModel
@@ -174,6 +179,30 @@ class ModelFactory:
174
179
  model_name=model_name,
175
180
  )
176
181
 
182
+ @staticmethod
183
+ def create_llm_gate_model(
184
+ model_name: str | None,
185
+ base_url: str,
186
+ provider: str = "default",
187
+ embedding_provider: str | None = None,
188
+ embedding_model_name: str | None = None,
189
+ user_id: str | None = None,
190
+ project_id: str | None = None,
191
+ ) -> LLMGateModel:
192
+ if LLMGateModel is None:
193
+ raise ImportError(
194
+ "Provider 'llm_gate' requires optional dependency 'donkit-llm-gate-client'"
195
+ )
196
+ return LLMGateModel(
197
+ base_url=base_url,
198
+ provider=provider,
199
+ model_name=model_name,
200
+ embedding_provider=embedding_provider,
201
+ embedding_model_name=embedding_model_name,
202
+ user_id=user_id,
203
+ project_id=project_id,
204
+ )
205
+
177
206
  @staticmethod
178
207
  def create_model(
179
208
  provider: Literal[
@@ -184,6 +213,7 @@ class ModelFactory:
184
213
  "vertex",
185
214
  "ollama",
186
215
  "donkit",
216
+ "llm_gate",
187
217
  ],
188
218
  model_name: str | None,
189
219
  credentials: dict,
@@ -198,6 +228,7 @@ class ModelFactory:
198
228
  "vertex": "gemini-2.5-flash",
199
229
  "ollama": "mistral",
200
230
  "donkit": None,
231
+ "llm_gate": None,
201
232
  }
202
233
  model_name = default_models.get(provider, "default")
203
234
  if provider == "openai":
@@ -258,5 +289,15 @@ class ModelFactory:
258
289
  api_key=credentials["api_key"],
259
290
  base_url=credentials["base_url"],
260
291
  )
292
+ elif provider == "llm_gate":
293
+ return ModelFactory.create_llm_gate_model(
294
+ model_name=model_name,
295
+ base_url=credentials["base_url"],
296
+ provider=credentials.get("provider", "default"),
297
+ embedding_provider=credentials.get("embedding_provider"),
298
+ embedding_model_name=credentials.get("embedding_model_name"),
299
+ user_id=credentials.get("user_id"),
300
+ project_id=credentials.get("project_id"),
301
+ )
261
302
  else:
262
303
  raise ValueError(f"Unknown provider: {provider}")
@@ -0,0 +1,210 @@
1
+ from typing import Any, AsyncIterator
2
+
3
+ from .model_abstract import (
4
+ EmbeddingRequest,
5
+ EmbeddingResponse,
6
+ FunctionCall,
7
+ GenerateRequest,
8
+ GenerateResponse,
9
+ LLMModelAbstract,
10
+ Message,
11
+ ModelCapability,
12
+ StreamChunk,
13
+ Tool,
14
+ ToolCall,
15
+ )
16
+
17
+
18
+ class LLMGateModel(LLMModelAbstract):
19
+ name = "llm_gate"
20
+
21
+ @staticmethod
22
+ def _get_client() -> type:
23
+ try:
24
+ from donkit.llm_gate.client import LLMGate
25
+
26
+ return LLMGate
27
+ except Exception as e:
28
+ raise ImportError(
29
+ "LLMGateModel requires 'donkit-llm-gate-client' to be installed"
30
+ ) from e
31
+
32
+ def __init__(
33
+ self,
34
+ base_url: str = "http://localhost:8002",
35
+ provider: str = "default",
36
+ model_name: str | None = None,
37
+ embedding_provider: str | None = None,
38
+ embedding_model_name: str | None = None,
39
+ user_id: str | None = None,
40
+ project_id: str | None = None,
41
+ ):
42
+ self.base_url = base_url
43
+ self.provider = provider
44
+ self._model_name = model_name
45
+ self.embedding_provider = embedding_provider
46
+ self.embedding_model_name = embedding_model_name
47
+ self.user_id = user_id
48
+ self.project_id = project_id
49
+ self._capabilities = self._determine_capabilities()
50
+
51
+ @property
52
+ def model_name(self) -> str:
53
+ return self._model_name or "default"
54
+
55
+ @model_name.setter
56
+ def model_name(self, value: str):
57
+ self._model_name = value
58
+ self._capabilities = self._determine_capabilities()
59
+
60
+ @property
61
+ def capabilities(self) -> ModelCapability:
62
+ return self._capabilities
63
+
64
+ def _determine_capabilities(self) -> ModelCapability:
65
+ caps = (
66
+ ModelCapability.TEXT_GENERATION
67
+ | ModelCapability.STREAMING
68
+ | ModelCapability.STRUCTURED_OUTPUT
69
+ | ModelCapability.TOOL_CALLING
70
+ | ModelCapability.MULTIMODAL_INPUT
71
+ | ModelCapability.EMBEDDINGS
72
+ )
73
+ return caps
74
+
75
+ def _convert_message(self, msg: Message) -> dict:
76
+ result: dict[str, Any] = {"role": msg.role}
77
+ if isinstance(msg.content, str):
78
+ result["content"] = msg.content
79
+ else:
80
+ content_parts = []
81
+ for part in msg.content if msg.content else []:
82
+ content_parts.append(part.model_dump(exclude_none=True))
83
+ result["content"] = content_parts
84
+ if msg.tool_calls:
85
+ result["tool_calls"] = [tc.model_dump() for tc in msg.tool_calls]
86
+ if msg.tool_call_id:
87
+ result["tool_call_id"] = msg.tool_call_id
88
+ if msg.name:
89
+ result["name"] = msg.name
90
+ return result
91
+
92
+ def _convert_tools(self, tools: list[Tool]) -> list[dict]:
93
+ return [tool.model_dump(exclude_none=True) for tool in tools]
94
+
95
+ def _prepare_generate_kwargs(self, request: GenerateRequest) -> dict:
96
+ messages = [self._convert_message(msg) for msg in request.messages]
97
+ tools_payload = self._convert_tools(request.tools) if request.tools else None
98
+
99
+ kwargs: dict[str, Any] = {
100
+ "provider": self.provider,
101
+ "model_name": self.model_name,
102
+ "messages": messages,
103
+ "user_id": self.user_id,
104
+ "project_id": self.project_id,
105
+ }
106
+
107
+ if request.temperature is not None:
108
+ kwargs["temperature"] = request.temperature
109
+ if request.max_tokens is not None:
110
+ kwargs["max_tokens"] = request.max_tokens
111
+ if request.top_p is not None:
112
+ kwargs["top_p"] = request.top_p
113
+ if request.stop:
114
+ kwargs["stop"] = request.stop
115
+ if tools_payload:
116
+ kwargs["tools"] = tools_payload
117
+ if request.tool_choice:
118
+ if isinstance(request.tool_choice, (str, dict)):
119
+ kwargs["tool_choice"] = request.tool_choice
120
+ else:
121
+ kwargs["tool_choice"] = "auto"
122
+ if request.response_format:
123
+ kwargs["response_format"] = request.response_format
124
+
125
+ return kwargs
126
+
127
+ async def generate(self, request: GenerateRequest) -> GenerateResponse:
128
+ await self.validate_request(request)
129
+
130
+ kwargs = self._prepare_generate_kwargs(request)
131
+
132
+ llm_gate = self._get_client()
133
+
134
+ async with llm_gate(base_url=self.base_url) as client:
135
+ response = await client.generate(**kwargs)
136
+
137
+ tool_calls = None
138
+ if response.tool_calls:
139
+ tool_calls = [
140
+ ToolCall(
141
+ id=tc.id,
142
+ type=tc.type,
143
+ function=FunctionCall(
144
+ name=tc.function.name,
145
+ arguments=tc.function.arguments,
146
+ ),
147
+ )
148
+ for tc in response.tool_calls
149
+ ]
150
+
151
+ return GenerateResponse(
152
+ content=response.content,
153
+ tool_calls=tool_calls,
154
+ finish_reason=response.finish_reason,
155
+ usage=response.usage,
156
+ metadata=response.metadata,
157
+ )
158
+
159
+ async def generate_stream(
160
+ self, request: GenerateRequest
161
+ ) -> AsyncIterator[StreamChunk]:
162
+ await self.validate_request(request)
163
+
164
+ kwargs = self._prepare_generate_kwargs(request)
165
+
166
+ llm_gate = self._get_client()
167
+
168
+ async with llm_gate(base_url=self.base_url) as client:
169
+ async for chunk in client.generate_stream(**kwargs):
170
+ tool_calls = None
171
+ if chunk.tool_calls:
172
+ tool_calls = [
173
+ ToolCall(
174
+ id=tc.id,
175
+ type=tc.type,
176
+ function=FunctionCall(
177
+ name=tc.function.name,
178
+ arguments=tc.function.arguments,
179
+ ),
180
+ )
181
+ for tc in chunk.tool_calls
182
+ ]
183
+
184
+ yield StreamChunk(
185
+ content=chunk.content,
186
+ tool_calls=tool_calls,
187
+ finish_reason=chunk.finish_reason,
188
+ )
189
+
190
+ async def embed(self, request: EmbeddingRequest) -> EmbeddingResponse:
191
+ provider = self.embedding_provider or self.provider
192
+ model_name = self.embedding_model_name
193
+
194
+ llm_gate = self._get_client()
195
+
196
+ async with llm_gate(base_url=self.base_url) as client:
197
+ response = await client.embeddings(
198
+ provider=provider,
199
+ input=request.input,
200
+ model_name=model_name,
201
+ dimensions=request.dimensions,
202
+ user_id=self.user_id,
203
+ project_id=self.project_id,
204
+ )
205
+
206
+ return EmbeddingResponse(
207
+ embeddings=response.embeddings,
208
+ usage=response.usage,
209
+ metadata=response.metadata,
210
+ )