gigamux 0.2.0__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.
- gigamux-0.2.0/.gitignore +6 -0
- gigamux-0.2.0/LICENSE +35 -0
- gigamux-0.2.0/PKG-INFO +260 -0
- gigamux-0.2.0/README.md +230 -0
- gigamux-0.2.0/pyproject.toml +58 -0
- gigamux-0.2.0/src/gigamux/__init__.py +52 -0
- gigamux-0.2.0/src/gigamux/adapters/__init__.py +1 -0
- gigamux-0.2.0/src/gigamux/adapters/embeddings.py +69 -0
- gigamux-0.2.0/src/gigamux/adapters/function_calling.py +66 -0
- gigamux-0.2.0/src/gigamux/adapters/langchain_chat.py +370 -0
- gigamux-0.2.0/src/gigamux/client.py +504 -0
- gigamux-0.2.0/src/gigamux/config.py +150 -0
- gigamux-0.2.0/src/gigamux/errors.py +54 -0
- gigamux-0.2.0/src/gigamux/limits/__init__.py +1 -0
- gigamux-0.2.0/src/gigamux/limits/base.py +27 -0
- gigamux-0.2.0/src/gigamux/limits/fallback.py +125 -0
- gigamux-0.2.0/src/gigamux/limits/local.py +47 -0
- gigamux-0.2.0/src/gigamux/limits/redis.py +88 -0
- gigamux-0.2.0/src/gigamux/models.py +135 -0
- gigamux-0.2.0/src/gigamux/pool.py +64 -0
- gigamux-0.2.0/src/gigamux/py.typed +0 -0
- gigamux-0.2.0/src/gigamux/redis_factory.py +54 -0
- gigamux-0.2.0/src/gigamux/stop_parser.py +91 -0
- gigamux-0.2.0/src/gigamux/stops/__init__.py +1 -0
- gigamux-0.2.0/src/gigamux/stops/base.py +20 -0
- gigamux-0.2.0/src/gigamux/stops/local.py +27 -0
- gigamux-0.2.0/src/gigamux/stops/redis.py +100 -0
- gigamux-0.2.0/src/gigamux/transport.py +53 -0
- gigamux-0.2.0/tests/__init__.py +0 -0
- gigamux-0.2.0/tests/conftest.py +129 -0
- gigamux-0.2.0/tests/test_adapter_chat.py +164 -0
- gigamux-0.2.0/tests/test_adapter_embeddings.py +94 -0
- gigamux-0.2.0/tests/test_adapter_structured.py +201 -0
- gigamux-0.2.0/tests/test_adapter_tools.py +361 -0
- gigamux-0.2.0/tests/test_client_cancellation.py +173 -0
- gigamux-0.2.0/tests/test_client_errors.py +231 -0
- gigamux-0.2.0/tests/test_client_redis_e2e.py +91 -0
- gigamux-0.2.0/tests/test_client_stream.py +215 -0
- gigamux-0.2.0/tests/test_client_unary.py +158 -0
- gigamux-0.2.0/tests/test_config.py +141 -0
- gigamux-0.2.0/tests/test_langgraph_smoke.py +72 -0
- gigamux-0.2.0/tests/test_limits_fallback.py +162 -0
- gigamux-0.2.0/tests/test_limits_leak.py +36 -0
- gigamux-0.2.0/tests/test_limits_local.py +48 -0
- gigamux-0.2.0/tests/test_limits_redis.py +52 -0
- gigamux-0.2.0/tests/test_models.py +109 -0
- gigamux-0.2.0/tests/test_pool.py +61 -0
- gigamux-0.2.0/tests/test_public_api.py +12 -0
- gigamux-0.2.0/tests/test_redis_factory.py +120 -0
- gigamux-0.2.0/tests/test_stop_parser.py +115 -0
- gigamux-0.2.0/tests/test_stops.py +196 -0
- gigamux-0.2.0/tests/test_transport.py +63 -0
gigamux-0.2.0/.gitignore
ADDED
gigamux-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
PROPRIETARY LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kalentev Leon Konstantinovich. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Subject to the terms of this license, the copyright holder grants any
|
|
6
|
+
person obtaining a copy of this software ("the Software") from the Python
|
|
7
|
+
Package Index a limited, non-exclusive, non-transferable, revocable
|
|
8
|
+
permission to install and use unmodified copies of the Software solely in
|
|
9
|
+
its distributed form.
|
|
10
|
+
|
|
11
|
+
Except for the limited permission expressly granted above, all rights are
|
|
12
|
+
reserved. Without prior written permission of the copyright holder, you
|
|
13
|
+
may NOT:
|
|
14
|
+
|
|
15
|
+
1. modify, adapt, translate, or create derivative works of the Software
|
|
16
|
+
or any part of it;
|
|
17
|
+
2. redistribute, sublicense, sell, rent, lease, lend, or otherwise
|
|
18
|
+
transfer the Software or access to it, in source or binary form;
|
|
19
|
+
3. incorporate the Software or any part of its source code into other
|
|
20
|
+
software;
|
|
21
|
+
4. remove or alter this license, the copyright notice, or any attribution;
|
|
22
|
+
5. publish benchmarks or reverse-engineer the Software except to the
|
|
23
|
+
extent expressly permitted by applicable law notwithstanding this
|
|
24
|
+
limitation.
|
|
25
|
+
|
|
26
|
+
Any use of the Software outside the scope of this license automatically
|
|
27
|
+
terminates the permission granted above.
|
|
28
|
+
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
32
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
33
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
|
34
|
+
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
35
|
+
SOFTWARE.
|
gigamux-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gigamux
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: GigaChat client with certificate pool, shared limits and stop events
|
|
5
|
+
Author-email: Kalentev Leon Konstantinovich <itqop@icloud.com>
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: coredis>=6.7
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: loguru>=0.7
|
|
19
|
+
Requires-Dist: pydantic>=2.7
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: fakeredis[lua]>=2.25; extra == 'dev'
|
|
22
|
+
Requires-Dist: langchain-core>=1.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: langgraph>=1.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
27
|
+
Provides-Extra: langchain
|
|
28
|
+
Requires-Dist: langchain-core>=1.0; extra == 'langchain'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# gigamux
|
|
32
|
+
|
|
33
|
+
Асинхронный клиент GigaChat: пул сертификатов-каналов, общие in-flight лимиты
|
|
34
|
+
через Redis (coredis) с fallback на локальные счётчики, stop-события (429 + время
|
|
35
|
+
блокировки) и drop-in адаптеры LangChain. Сервисы переходят на библиотеку заменой
|
|
36
|
+
импорта, retry-логика и троттлинг живут в ядре.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
Ядро напрямую:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from gigamux import GigaCoreClient, config_from_env, ChatRequest, Message
|
|
44
|
+
|
|
45
|
+
config = config_from_env()
|
|
46
|
+
async with GigaCoreClient(config) as client:
|
|
47
|
+
result = await client.chat(
|
|
48
|
+
ChatRequest(
|
|
49
|
+
model="GigaChat-2-Max",
|
|
50
|
+
messages=(Message(role="user", content="привет"),),
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
print(result.content, result.meta.channel)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
LangChain-сервисы (адаптеры импортируются напрямую, требуют extra `langchain`):
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from gigamux.adapters.langchain_chat import PooledGigaChat
|
|
60
|
+
|
|
61
|
+
llm = PooledGigaChat(core=client, model="GigaChat-2-Max", temperature=0.1)
|
|
62
|
+
message = await llm.ainvoke("привет")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Индексация (один батч = один HTTP-запрос = один слот, нарезка по 20 в адаптере):
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from gigamux.adapters.embeddings import PooledGigaEmbeddings
|
|
69
|
+
|
|
70
|
+
embedder = PooledGigaEmbeddings(core=client, model="EmbeddingsGigaR")
|
|
71
|
+
vectors = await embedder.aembed_documents(texts)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Function calling и structured output
|
|
75
|
+
|
|
76
|
+
`PooledGigaChat` поддерживает инструменты и структурированный вывод —
|
|
77
|
+
паритет с langchain-gigachat, включая react-агентов LangGraph:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from langchain_core.tools import tool
|
|
81
|
+
from langgraph.prebuilt import create_react_agent
|
|
82
|
+
|
|
83
|
+
@tool
|
|
84
|
+
def get_weather(city: str) -> str:
|
|
85
|
+
"""Return current weather for a city."""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
agent = create_react_agent(llm, [get_weather])
|
|
89
|
+
state = await agent.ainvoke({"messages": [...]})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Extra `gigamux[langchain]` ставит только langchain-core; для примера выше
|
|
93
|
+
нужен отдельно установленный langgraph (`pip install langgraph`).
|
|
94
|
+
|
|
95
|
+
Структурированный вывод доступен двумя методами эталонной библиотеки:
|
|
96
|
+
`function_calling` (по умолчанию) и `json_mode` (через `response_format`).
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
chain = llm.with_structured_output(MyModel)
|
|
100
|
+
chain = llm.with_structured_output(MyModel, method="json_mode")
|
|
101
|
+
result = await chain.ainvoke("...")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Ограничения и семантика:
|
|
105
|
+
|
|
106
|
+
- Стриминг с инструментами на уровне API невозможен, поэтому
|
|
107
|
+
`astream`/`astream_events` с привязанными функциями прозрачно выполняют
|
|
108
|
+
обычный вызов и отдают ответ одним чанком: токенного стриминга нет, но
|
|
109
|
+
react-агенты под `astream_events`/`stream_mode="messages"` работают.
|
|
110
|
+
- Параллельные tool_calls невозможны на стороне GigaChat: больше одного
|
|
111
|
+
tool_call в сообщении — `ValueError`.
|
|
112
|
+
- В `with_structured_output` поддерживаются pydantic-модели обоих поколений
|
|
113
|
+
(v2 и `pydantic.v1`) в обоих методах; v1-класс возвращается экземпляром,
|
|
114
|
+
как и v2.
|
|
115
|
+
- `finish_reason` доступен в `response_metadata` каждого ответа адаптера.
|
|
116
|
+
- `with_structured_output` методом `function_calling` возвращает `None`,
|
|
117
|
+
если модель ответила текстом вместо вызова функции (поведение эталона);
|
|
118
|
+
с `include_raw=True` сырое сообщение доступно в `output["raw"]`.
|
|
119
|
+
- `json_mode` использует бета-фичу API `response_format` — при её
|
|
120
|
+
отсутствии на контуре ошибка идентична поведению langchain-gigachat.
|
|
121
|
+
- `few_shot_examples` пробрасываются из `metadata` инструмента.
|
|
122
|
+
|
|
123
|
+
## Стриминг
|
|
124
|
+
|
|
125
|
+
Ядро отдаёт дельты как `StreamChunk`; слот канала занят весь стрим, метаданные
|
|
126
|
+
приходят в последнем чанке:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
request = ChatRequest(
|
|
130
|
+
model="GigaChat-2-Max",
|
|
131
|
+
messages=(Message(role="user", content="расскажи анекдот"),),
|
|
132
|
+
)
|
|
133
|
+
async for chunk in client.stream(request):
|
|
134
|
+
print(chunk.delta, end="")
|
|
135
|
+
if chunk.meta is not None:
|
|
136
|
+
print(chunk.meta.channel, chunk.meta.duration_ms)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Ошибки до первого чанка ретраятся как обычные запросы; после начала стрима —
|
|
140
|
+
`StreamInterruptedError` без молчаливого ретрая. У адаптера работает
|
|
141
|
+
`llm.astream(...)`; без инструментов это настоящий токенный стриминг, а с
|
|
142
|
+
привязанными функциями ответ приходит одним финальным чанком (см. раздел
|
|
143
|
+
про function calling).
|
|
144
|
+
|
|
145
|
+
Потребитель, который может бросить стрим до конца, должен оборачивать его в
|
|
146
|
+
`contextlib.aclosing(...)`, иначе слот освобождается только сборщиком мусора.
|
|
147
|
+
Keepalive слота ограничен `slot_max_lifetime` (дефолт 900 с): после этого
|
|
148
|
+
лимита lease перестаёт продлеваться и истекает по TTL даже без финализации
|
|
149
|
+
генератора.
|
|
150
|
+
|
|
151
|
+
## Конфигурация в коде
|
|
152
|
+
|
|
153
|
+
`config_from_env()` — удобный путь, но конфиг можно собрать руками:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from gigamux import ChannelConfig, ClientConfig, GigaCoreClient
|
|
157
|
+
|
|
158
|
+
config = ClientConfig(
|
|
159
|
+
channels=[
|
|
160
|
+
ChannelConfig(
|
|
161
|
+
name="cert-a",
|
|
162
|
+
base_url="https://giga.internal:10501/v1",
|
|
163
|
+
cert_file="/etc/certs/a/tls.pem",
|
|
164
|
+
key_file="/etc/certs/a/key.pem",
|
|
165
|
+
ca_bundle="/etc/certs/ca.pem",
|
|
166
|
+
limits={"GigaChat-2-Max": 2, "EmbeddingsGigaR": 4},
|
|
167
|
+
),
|
|
168
|
+
ChannelConfig(
|
|
169
|
+
name="cert-b",
|
|
170
|
+
base_url="https://giga.internal:10502/v1",
|
|
171
|
+
limits={"*": 3},
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
redis_url="rediss://redis.internal:6380/0",
|
|
175
|
+
redis_username="svc",
|
|
176
|
+
redis_password="...",
|
|
177
|
+
acquire_timeout=30.0,
|
|
178
|
+
max_retries=3,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async with GigaCoreClient(config) as client:
|
|
182
|
+
...
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Ключевые поля `ClientConfig` (все имеют дефолты):
|
|
186
|
+
|
|
187
|
+
| Поле | Дефолт | Назначение |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `acquire_timeout` | 30.0 | сколько ждать свободный слот; `None` — ждать без таймаута (индексация) |
|
|
190
|
+
| `lease_ttl` / `heartbeat_interval` | 120 / ttl/3 | время жизни слота в Redis и период продления |
|
|
191
|
+
| `slot_max_lifetime` | 900.0 | максимум продления слота keepalive-ом; дальше lease истекает по TTL |
|
|
192
|
+
| `max_retries` / `backoff_factor` / `backoff_max` | 3 / 1.0 / 30.0 | ретраи 429/5xx с экспоненциальной паузой |
|
|
193
|
+
| `connect_timeout` / `read_timeout` | 10 / 120 | таймауты HTTP |
|
|
194
|
+
| `redis_failure_threshold` / `redis_probe_interval` | 3 / 15.0 | circuit breaker: после N ошибок Redis лимиты локальные, проба каждые 15 с |
|
|
195
|
+
|
|
196
|
+
`limits` канала — единственный источник правды о том, какие модели канал
|
|
197
|
+
обслуживает: ключ — имя модели, значение — число одновременных запросов,
|
|
198
|
+
`"*"` — wildcard. `cert_file`/`key_file` задаются только вместе; без них
|
|
199
|
+
канал работает без mTLS. `acquire_timeout` можно переопределить и на один
|
|
200
|
+
вызов: `client.chat(request, acquire_timeout=None)`.
|
|
201
|
+
|
|
202
|
+
## Переменные окружения
|
|
203
|
+
|
|
204
|
+
| Переменная | Назначение |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `GIGA_CHANNELS` | JSON-список каналов (`name`, `base_url`, `cert_file`, `key_file`, `ca_bundle`, `limits`) |
|
|
207
|
+
| `GIGA_REDIS_URL` | URL Redis для распределённых лимитов; не задан -> локальные лимиты |
|
|
208
|
+
| `GIGA_REDIS_USERNAME` / `GIGA_REDIS_PASSWORD` | аутентификация Redis |
|
|
209
|
+
| `GIGA_REDIS_KEY_FILE` / `GIGA_REDIS_CERT_FILE` / `GIGA_REDIS_CA_BUNDLE` | mTLS для Redis |
|
|
210
|
+
| `GIGACHAT_HOST` / `GIGACHAT_PORT` / `GIGACHAT_TLS_CERT_FILEPATH` / `GIGACHAT_KEY_FILEPATH` / `GIGACHAT_CA_BUNDLE_FILEPATH` | режим совместимости: один канал из legacy-переменных |
|
|
211
|
+
| `GIGACHAT_ENDPOINT` | legacy-путь base_url в режиме совместимости (по умолчанию `/v1`) |
|
|
212
|
+
| `GIGACHAT_MAX_CONCURRENCY` | лимит на канал в режиме совместимости (wildcard-модель) |
|
|
213
|
+
|
|
214
|
+
Если задан `GIGA_CHANNELS`, он имеет приоритет. Иначе из `GIGACHAT_*` строится
|
|
215
|
+
пул из одного канала с wildcard-лимитом, что даёт миграцию без изменения конфига.
|
|
216
|
+
|
|
217
|
+
## Семантика
|
|
218
|
+
|
|
219
|
+
Лимит — in-flight слоты на пару канал+модель, общие между репликами. Слот берётся
|
|
220
|
+
как lease с TTL в Redis и продлевается heartbeat-ом, поэтому упавший под не держит
|
|
221
|
+
слот навсегда. При занятых слотах вызов ждёт в очереди с джиттером до
|
|
222
|
+
`acquire_timeout` (онлайн ~30 с, индексация — без таймаута), затем `SlotWaitTimeoutError`.
|
|
223
|
+
Stop-событие выводит канал из ротации до времени T и прозрачно ретраит на другом
|
|
224
|
+
канале; когда все каналы в стопе — сразу `AllChannelsStoppedError(resume_at)`.
|
|
225
|
+
Ошибка посреди стрима не ретраится молча — `StreamInterruptedError`.
|
|
226
|
+
|
|
227
|
+
## Ошибки
|
|
228
|
+
|
|
229
|
+
Все исключения наследуют `GigaClientError` и импортируются из `gigamux`:
|
|
230
|
+
|
|
231
|
+
| Исключение | Когда летит |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `ConfigError` | невалидная конфигурация или переменные окружения |
|
|
234
|
+
| `NoChannelForModelError` | ни один канал не обслуживает запрошенную модель |
|
|
235
|
+
| `SlotWaitTimeoutError` | свободный слот не появился за `acquire_timeout` |
|
|
236
|
+
| `AllChannelsStoppedError` | все каналы в стопе; `resume_at` — когда вернётся первый |
|
|
237
|
+
| `RateLimitError` | 429 пережил все ретраи |
|
|
238
|
+
| `ServerError` | 5xx или сетевая ошибка пережили все ретраи |
|
|
239
|
+
| `ApiError` | прочие неретраябельные ответы API (4xx); базовый класс двух предыдущих |
|
|
240
|
+
| `StreamInterruptedError` | стрим оборвался после уже отданных чанков |
|
|
241
|
+
|
|
242
|
+
У `ApiError` и наследников доступны `status_code`, `body`, `channel`,
|
|
243
|
+
`request_id`. Типовая обработка: `SlotWaitTimeoutError` и `AllChannelsStoppedError`
|
|
244
|
+
— перегрузка, имеет смысл отдать 429/503 наверх; `ApiError` — ошибка запроса,
|
|
245
|
+
ретраить бесполезно.
|
|
246
|
+
|
|
247
|
+
## Метаданные ответа
|
|
248
|
+
|
|
249
|
+
Каждый результат (`ChatResult`, `EmbedResult`, финальный `StreamChunk`) несёт
|
|
250
|
+
`meta: ResponseMeta` — `request_id`, `headers`, `channel`, `model`, `attempts`,
|
|
251
|
+
`duration_ms`, `status_code`. Это сквозной способ узнать, через какой канал
|
|
252
|
+
ушёл запрос и сколько он занял, например для логов и метрик.
|
|
253
|
+
|
|
254
|
+
## Тесты
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
python -m venv .venv
|
|
258
|
+
.venv\Scripts\pip install -e ".[dev]"
|
|
259
|
+
.venv\Scripts\python -m pytest
|
|
260
|
+
```
|
gigamux-0.2.0/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# gigamux
|
|
2
|
+
|
|
3
|
+
Асинхронный клиент GigaChat: пул сертификатов-каналов, общие in-flight лимиты
|
|
4
|
+
через Redis (coredis) с fallback на локальные счётчики, stop-события (429 + время
|
|
5
|
+
блокировки) и drop-in адаптеры LangChain. Сервисы переходят на библиотеку заменой
|
|
6
|
+
импорта, retry-логика и троттлинг живут в ядре.
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
|
|
10
|
+
Ядро напрямую:
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from gigamux import GigaCoreClient, config_from_env, ChatRequest, Message
|
|
14
|
+
|
|
15
|
+
config = config_from_env()
|
|
16
|
+
async with GigaCoreClient(config) as client:
|
|
17
|
+
result = await client.chat(
|
|
18
|
+
ChatRequest(
|
|
19
|
+
model="GigaChat-2-Max",
|
|
20
|
+
messages=(Message(role="user", content="привет"),),
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
print(result.content, result.meta.channel)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
LangChain-сервисы (адаптеры импортируются напрямую, требуют extra `langchain`):
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from gigamux.adapters.langchain_chat import PooledGigaChat
|
|
30
|
+
|
|
31
|
+
llm = PooledGigaChat(core=client, model="GigaChat-2-Max", temperature=0.1)
|
|
32
|
+
message = await llm.ainvoke("привет")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Индексация (один батч = один HTTP-запрос = один слот, нарезка по 20 в адаптере):
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from gigamux.adapters.embeddings import PooledGigaEmbeddings
|
|
39
|
+
|
|
40
|
+
embedder = PooledGigaEmbeddings(core=client, model="EmbeddingsGigaR")
|
|
41
|
+
vectors = await embedder.aembed_documents(texts)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Function calling и structured output
|
|
45
|
+
|
|
46
|
+
`PooledGigaChat` поддерживает инструменты и структурированный вывод —
|
|
47
|
+
паритет с langchain-gigachat, включая react-агентов LangGraph:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from langchain_core.tools import tool
|
|
51
|
+
from langgraph.prebuilt import create_react_agent
|
|
52
|
+
|
|
53
|
+
@tool
|
|
54
|
+
def get_weather(city: str) -> str:
|
|
55
|
+
"""Return current weather for a city."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
agent = create_react_agent(llm, [get_weather])
|
|
59
|
+
state = await agent.ainvoke({"messages": [...]})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Extra `gigamux[langchain]` ставит только langchain-core; для примера выше
|
|
63
|
+
нужен отдельно установленный langgraph (`pip install langgraph`).
|
|
64
|
+
|
|
65
|
+
Структурированный вывод доступен двумя методами эталонной библиотеки:
|
|
66
|
+
`function_calling` (по умолчанию) и `json_mode` (через `response_format`).
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
chain = llm.with_structured_output(MyModel)
|
|
70
|
+
chain = llm.with_structured_output(MyModel, method="json_mode")
|
|
71
|
+
result = await chain.ainvoke("...")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Ограничения и семантика:
|
|
75
|
+
|
|
76
|
+
- Стриминг с инструментами на уровне API невозможен, поэтому
|
|
77
|
+
`astream`/`astream_events` с привязанными функциями прозрачно выполняют
|
|
78
|
+
обычный вызов и отдают ответ одним чанком: токенного стриминга нет, но
|
|
79
|
+
react-агенты под `astream_events`/`stream_mode="messages"` работают.
|
|
80
|
+
- Параллельные tool_calls невозможны на стороне GigaChat: больше одного
|
|
81
|
+
tool_call в сообщении — `ValueError`.
|
|
82
|
+
- В `with_structured_output` поддерживаются pydantic-модели обоих поколений
|
|
83
|
+
(v2 и `pydantic.v1`) в обоих методах; v1-класс возвращается экземпляром,
|
|
84
|
+
как и v2.
|
|
85
|
+
- `finish_reason` доступен в `response_metadata` каждого ответа адаптера.
|
|
86
|
+
- `with_structured_output` методом `function_calling` возвращает `None`,
|
|
87
|
+
если модель ответила текстом вместо вызова функции (поведение эталона);
|
|
88
|
+
с `include_raw=True` сырое сообщение доступно в `output["raw"]`.
|
|
89
|
+
- `json_mode` использует бета-фичу API `response_format` — при её
|
|
90
|
+
отсутствии на контуре ошибка идентична поведению langchain-gigachat.
|
|
91
|
+
- `few_shot_examples` пробрасываются из `metadata` инструмента.
|
|
92
|
+
|
|
93
|
+
## Стриминг
|
|
94
|
+
|
|
95
|
+
Ядро отдаёт дельты как `StreamChunk`; слот канала занят весь стрим, метаданные
|
|
96
|
+
приходят в последнем чанке:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
request = ChatRequest(
|
|
100
|
+
model="GigaChat-2-Max",
|
|
101
|
+
messages=(Message(role="user", content="расскажи анекдот"),),
|
|
102
|
+
)
|
|
103
|
+
async for chunk in client.stream(request):
|
|
104
|
+
print(chunk.delta, end="")
|
|
105
|
+
if chunk.meta is not None:
|
|
106
|
+
print(chunk.meta.channel, chunk.meta.duration_ms)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Ошибки до первого чанка ретраятся как обычные запросы; после начала стрима —
|
|
110
|
+
`StreamInterruptedError` без молчаливого ретрая. У адаптера работает
|
|
111
|
+
`llm.astream(...)`; без инструментов это настоящий токенный стриминг, а с
|
|
112
|
+
привязанными функциями ответ приходит одним финальным чанком (см. раздел
|
|
113
|
+
про function calling).
|
|
114
|
+
|
|
115
|
+
Потребитель, который может бросить стрим до конца, должен оборачивать его в
|
|
116
|
+
`contextlib.aclosing(...)`, иначе слот освобождается только сборщиком мусора.
|
|
117
|
+
Keepalive слота ограничен `slot_max_lifetime` (дефолт 900 с): после этого
|
|
118
|
+
лимита lease перестаёт продлеваться и истекает по TTL даже без финализации
|
|
119
|
+
генератора.
|
|
120
|
+
|
|
121
|
+
## Конфигурация в коде
|
|
122
|
+
|
|
123
|
+
`config_from_env()` — удобный путь, но конфиг можно собрать руками:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from gigamux import ChannelConfig, ClientConfig, GigaCoreClient
|
|
127
|
+
|
|
128
|
+
config = ClientConfig(
|
|
129
|
+
channels=[
|
|
130
|
+
ChannelConfig(
|
|
131
|
+
name="cert-a",
|
|
132
|
+
base_url="https://giga.internal:10501/v1",
|
|
133
|
+
cert_file="/etc/certs/a/tls.pem",
|
|
134
|
+
key_file="/etc/certs/a/key.pem",
|
|
135
|
+
ca_bundle="/etc/certs/ca.pem",
|
|
136
|
+
limits={"GigaChat-2-Max": 2, "EmbeddingsGigaR": 4},
|
|
137
|
+
),
|
|
138
|
+
ChannelConfig(
|
|
139
|
+
name="cert-b",
|
|
140
|
+
base_url="https://giga.internal:10502/v1",
|
|
141
|
+
limits={"*": 3},
|
|
142
|
+
),
|
|
143
|
+
],
|
|
144
|
+
redis_url="rediss://redis.internal:6380/0",
|
|
145
|
+
redis_username="svc",
|
|
146
|
+
redis_password="...",
|
|
147
|
+
acquire_timeout=30.0,
|
|
148
|
+
max_retries=3,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async with GigaCoreClient(config) as client:
|
|
152
|
+
...
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Ключевые поля `ClientConfig` (все имеют дефолты):
|
|
156
|
+
|
|
157
|
+
| Поле | Дефолт | Назначение |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `acquire_timeout` | 30.0 | сколько ждать свободный слот; `None` — ждать без таймаута (индексация) |
|
|
160
|
+
| `lease_ttl` / `heartbeat_interval` | 120 / ttl/3 | время жизни слота в Redis и период продления |
|
|
161
|
+
| `slot_max_lifetime` | 900.0 | максимум продления слота keepalive-ом; дальше lease истекает по TTL |
|
|
162
|
+
| `max_retries` / `backoff_factor` / `backoff_max` | 3 / 1.0 / 30.0 | ретраи 429/5xx с экспоненциальной паузой |
|
|
163
|
+
| `connect_timeout` / `read_timeout` | 10 / 120 | таймауты HTTP |
|
|
164
|
+
| `redis_failure_threshold` / `redis_probe_interval` | 3 / 15.0 | circuit breaker: после N ошибок Redis лимиты локальные, проба каждые 15 с |
|
|
165
|
+
|
|
166
|
+
`limits` канала — единственный источник правды о том, какие модели канал
|
|
167
|
+
обслуживает: ключ — имя модели, значение — число одновременных запросов,
|
|
168
|
+
`"*"` — wildcard. `cert_file`/`key_file` задаются только вместе; без них
|
|
169
|
+
канал работает без mTLS. `acquire_timeout` можно переопределить и на один
|
|
170
|
+
вызов: `client.chat(request, acquire_timeout=None)`.
|
|
171
|
+
|
|
172
|
+
## Переменные окружения
|
|
173
|
+
|
|
174
|
+
| Переменная | Назначение |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `GIGA_CHANNELS` | JSON-список каналов (`name`, `base_url`, `cert_file`, `key_file`, `ca_bundle`, `limits`) |
|
|
177
|
+
| `GIGA_REDIS_URL` | URL Redis для распределённых лимитов; не задан -> локальные лимиты |
|
|
178
|
+
| `GIGA_REDIS_USERNAME` / `GIGA_REDIS_PASSWORD` | аутентификация Redis |
|
|
179
|
+
| `GIGA_REDIS_KEY_FILE` / `GIGA_REDIS_CERT_FILE` / `GIGA_REDIS_CA_BUNDLE` | mTLS для Redis |
|
|
180
|
+
| `GIGACHAT_HOST` / `GIGACHAT_PORT` / `GIGACHAT_TLS_CERT_FILEPATH` / `GIGACHAT_KEY_FILEPATH` / `GIGACHAT_CA_BUNDLE_FILEPATH` | режим совместимости: один канал из legacy-переменных |
|
|
181
|
+
| `GIGACHAT_ENDPOINT` | legacy-путь base_url в режиме совместимости (по умолчанию `/v1`) |
|
|
182
|
+
| `GIGACHAT_MAX_CONCURRENCY` | лимит на канал в режиме совместимости (wildcard-модель) |
|
|
183
|
+
|
|
184
|
+
Если задан `GIGA_CHANNELS`, он имеет приоритет. Иначе из `GIGACHAT_*` строится
|
|
185
|
+
пул из одного канала с wildcard-лимитом, что даёт миграцию без изменения конфига.
|
|
186
|
+
|
|
187
|
+
## Семантика
|
|
188
|
+
|
|
189
|
+
Лимит — in-flight слоты на пару канал+модель, общие между репликами. Слот берётся
|
|
190
|
+
как lease с TTL в Redis и продлевается heartbeat-ом, поэтому упавший под не держит
|
|
191
|
+
слот навсегда. При занятых слотах вызов ждёт в очереди с джиттером до
|
|
192
|
+
`acquire_timeout` (онлайн ~30 с, индексация — без таймаута), затем `SlotWaitTimeoutError`.
|
|
193
|
+
Stop-событие выводит канал из ротации до времени T и прозрачно ретраит на другом
|
|
194
|
+
канале; когда все каналы в стопе — сразу `AllChannelsStoppedError(resume_at)`.
|
|
195
|
+
Ошибка посреди стрима не ретраится молча — `StreamInterruptedError`.
|
|
196
|
+
|
|
197
|
+
## Ошибки
|
|
198
|
+
|
|
199
|
+
Все исключения наследуют `GigaClientError` и импортируются из `gigamux`:
|
|
200
|
+
|
|
201
|
+
| Исключение | Когда летит |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `ConfigError` | невалидная конфигурация или переменные окружения |
|
|
204
|
+
| `NoChannelForModelError` | ни один канал не обслуживает запрошенную модель |
|
|
205
|
+
| `SlotWaitTimeoutError` | свободный слот не появился за `acquire_timeout` |
|
|
206
|
+
| `AllChannelsStoppedError` | все каналы в стопе; `resume_at` — когда вернётся первый |
|
|
207
|
+
| `RateLimitError` | 429 пережил все ретраи |
|
|
208
|
+
| `ServerError` | 5xx или сетевая ошибка пережили все ретраи |
|
|
209
|
+
| `ApiError` | прочие неретраябельные ответы API (4xx); базовый класс двух предыдущих |
|
|
210
|
+
| `StreamInterruptedError` | стрим оборвался после уже отданных чанков |
|
|
211
|
+
|
|
212
|
+
У `ApiError` и наследников доступны `status_code`, `body`, `channel`,
|
|
213
|
+
`request_id`. Типовая обработка: `SlotWaitTimeoutError` и `AllChannelsStoppedError`
|
|
214
|
+
— перегрузка, имеет смысл отдать 429/503 наверх; `ApiError` — ошибка запроса,
|
|
215
|
+
ретраить бесполезно.
|
|
216
|
+
|
|
217
|
+
## Метаданные ответа
|
|
218
|
+
|
|
219
|
+
Каждый результат (`ChatResult`, `EmbedResult`, финальный `StreamChunk`) несёт
|
|
220
|
+
`meta: ResponseMeta` — `request_id`, `headers`, `channel`, `model`, `attempts`,
|
|
221
|
+
`duration_ms`, `status_code`. Это сквозной способ узнать, через какой канал
|
|
222
|
+
ушёл запрос и сколько он занял, например для логов и метрик.
|
|
223
|
+
|
|
224
|
+
## Тесты
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
python -m venv .venv
|
|
228
|
+
.venv\Scripts\pip install -e ".[dev]"
|
|
229
|
+
.venv\Scripts\python -m pytest
|
|
230
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "gigamux"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "GigaChat client with certificate pool, shared limits and stop events"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "LicenseRef-Proprietary"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Kalentev Leon Konstantinovich", email = "itqop@icloud.com" },
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"httpx>=0.27",
|
|
23
|
+
"coredis>=6.7",
|
|
24
|
+
"pydantic>=2.7",
|
|
25
|
+
"loguru>=0.7",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
langchain = ["langchain-core>=1.0"]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8",
|
|
32
|
+
"pytest-asyncio>=0.24",
|
|
33
|
+
"fakeredis[lua]>=2.25",
|
|
34
|
+
"langchain-core>=1.0",
|
|
35
|
+
"langgraph>=1.0",
|
|
36
|
+
"ruff>=0.8",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 110
|
|
41
|
+
src = ["src", "tests"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
asyncio_mode = "auto"
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["hatchling"]
|
|
52
|
+
build-backend = "hatchling.build"
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/gigamux"]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.sdist]
|
|
58
|
+
only-include = ["src", "tests", "README.md", "pyproject.toml"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""GigaChat client with certificate pool, shared limits and stop events."""
|
|
2
|
+
|
|
3
|
+
from gigamux.client import GigaCoreClient
|
|
4
|
+
from gigamux.config import ChannelConfig, ClientConfig, config_from_env
|
|
5
|
+
from gigamux.errors import (
|
|
6
|
+
AllChannelsStoppedError,
|
|
7
|
+
ApiError,
|
|
8
|
+
ConfigError,
|
|
9
|
+
GigaClientError,
|
|
10
|
+
NoChannelForModelError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
ServerError,
|
|
13
|
+
SlotWaitTimeoutError,
|
|
14
|
+
StreamInterruptedError,
|
|
15
|
+
)
|
|
16
|
+
from gigamux.models import (
|
|
17
|
+
ChatRequest,
|
|
18
|
+
ChatResult,
|
|
19
|
+
EmbedResult,
|
|
20
|
+
Message,
|
|
21
|
+
ResponseMeta,
|
|
22
|
+
StopEvent,
|
|
23
|
+
StreamChunk,
|
|
24
|
+
Usage,
|
|
25
|
+
)
|
|
26
|
+
from gigamux.stop_parser import StopEventParser, default_stop_parser
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AllChannelsStoppedError",
|
|
30
|
+
"ApiError",
|
|
31
|
+
"ChannelConfig",
|
|
32
|
+
"ChatRequest",
|
|
33
|
+
"ChatResult",
|
|
34
|
+
"ClientConfig",
|
|
35
|
+
"ConfigError",
|
|
36
|
+
"EmbedResult",
|
|
37
|
+
"GigaClientError",
|
|
38
|
+
"GigaCoreClient",
|
|
39
|
+
"Message",
|
|
40
|
+
"NoChannelForModelError",
|
|
41
|
+
"RateLimitError",
|
|
42
|
+
"ResponseMeta",
|
|
43
|
+
"ServerError",
|
|
44
|
+
"SlotWaitTimeoutError",
|
|
45
|
+
"StopEvent",
|
|
46
|
+
"StopEventParser",
|
|
47
|
+
"StreamChunk",
|
|
48
|
+
"StreamInterruptedError",
|
|
49
|
+
"Usage",
|
|
50
|
+
"config_from_env",
|
|
51
|
+
"default_stop_parser",
|
|
52
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Optional framework adapters; importing them requires the extras installed."""
|