llm-mesh 1.0.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.
- llm_mesh-1.0.0/LICENSE +21 -0
- llm_mesh-1.0.0/MANIFEST.in +1 -0
- llm_mesh-1.0.0/PKG-INFO +508 -0
- llm_mesh-1.0.0/README.md +486 -0
- llm_mesh-1.0.0/examples/models.json +18 -0
- llm_mesh-1.0.0/pyproject.toml +33 -0
- llm_mesh-1.0.0/setup.cfg +4 -0
- llm_mesh-1.0.0/src/llm_mesh/__init__.py +48 -0
- llm_mesh-1.0.0/src/llm_mesh/_common.py +193 -0
- llm_mesh-1.0.0/src/llm_mesh/_retry.py +52 -0
- llm_mesh-1.0.0/src/llm_mesh/_streaming.py +206 -0
- llm_mesh-1.0.0/src/llm_mesh/canary.py +67 -0
- llm_mesh-1.0.0/src/llm_mesh/config.py +82 -0
- llm_mesh-1.0.0/src/llm_mesh/gigachat/__init__.py +9 -0
- llm_mesh-1.0.0/src/llm_mesh/gigachat/_common.py +131 -0
- llm_mesh-1.0.0/src/llm_mesh/gigachat/batch.py +542 -0
- llm_mesh-1.0.0/src/llm_mesh/gigachat/client.py +1311 -0
- llm_mesh-1.0.0/src/llm_mesh/hooks.py +40 -0
- llm_mesh-1.0.0/src/llm_mesh/models.json +3013 -0
- llm_mesh-1.0.0/src/llm_mesh/models_catalog.py +421 -0
- llm_mesh-1.0.0/src/llm_mesh/openai/__init__.py +6 -0
- llm_mesh-1.0.0/src/llm_mesh/openai/_common.py +132 -0
- llm_mesh-1.0.0/src/llm_mesh/openai/client.py +2144 -0
- llm_mesh-1.0.0/src/llm_mesh/openai/discovery.py +39 -0
- llm_mesh-1.0.0/src/llm_mesh/protocol.py +64 -0
- llm_mesh-1.0.0/src/llm_mesh/py.typed +0 -0
- llm_mesh-1.0.0/src/llm_mesh/stream_events.py +118 -0
- llm_mesh-1.0.0/src/llm_mesh/text_parsing.py +295 -0
- llm_mesh-1.0.0/src/llm_mesh/types.py +234 -0
- llm_mesh-1.0.0/src/llm_mesh.egg-info/PKG-INFO +508 -0
- llm_mesh-1.0.0/src/llm_mesh.egg-info/SOURCES.txt +56 -0
- llm_mesh-1.0.0/src/llm_mesh.egg-info/dependency_links.txt +1 -0
- llm_mesh-1.0.0/src/llm_mesh.egg-info/entry_points.txt +2 -0
- llm_mesh-1.0.0/src/llm_mesh.egg-info/requires.txt +11 -0
- llm_mesh-1.0.0/src/llm_mesh.egg-info/top_level.txt +1 -0
- llm_mesh-1.0.0/tests/test_canary.py +42 -0
- llm_mesh-1.0.0/tests/test_catalog_packaging.py +162 -0
- llm_mesh-1.0.0/tests/test_client_options.py +99 -0
- llm_mesh-1.0.0/tests/test_client_policy_regressions.py +78 -0
- llm_mesh-1.0.0/tests/test_client_transport_contracts.py +124 -0
- llm_mesh-1.0.0/tests/test_discovery.py +33 -0
- llm_mesh-1.0.0/tests/test_environment_config.py +120 -0
- llm_mesh-1.0.0/tests/test_extract_json_array_from_text.py +58 -0
- llm_mesh-1.0.0/tests/test_gigachat_batch.py +332 -0
- llm_mesh-1.0.0/tests/test_gigachat_structured_modes.py +492 -0
- llm_mesh-1.0.0/tests/test_hooks.py +25 -0
- llm_mesh-1.0.0/tests/test_llm_gigachat.py +1360 -0
- llm_mesh-1.0.0/tests/test_llm_openai.py +2724 -0
- llm_mesh-1.0.0/tests/test_llm_protocol.py +130 -0
- llm_mesh-1.0.0/tests/test_llm_truncation_warning_clipped.py +78 -0
- llm_mesh-1.0.0/tests/test_openai_gateway_5xx_tool_degrade.py +188 -0
- llm_mesh-1.0.0/tests/test_openai_multitool_corrupted_args.py +258 -0
- llm_mesh-1.0.0/tests/test_openai_open_object_schema.py +639 -0
- llm_mesh-1.0.0/tests/test_openai_reasoning_parity.py +41 -0
- llm_mesh-1.0.0/tests/test_preserve_fallback.py +108 -0
- llm_mesh-1.0.0/tests/test_stream_events.py +545 -0
- llm_mesh-1.0.0/tests/test_structured_schema_degradation.py +126 -0
- llm_mesh-1.0.0/tests/test_text_parsing.py +185 -0
llm_mesh-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergey Lizin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include examples/models.json
|
llm_mesh-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-mesh
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LLM clients with a shared model catalog
|
|
5
|
+
Author: Sergey Lizin
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/SergeyLizin/llm-mesh
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: httpx<1,>=0.27
|
|
12
|
+
Requires-Dist: pydantic<3,>=2
|
|
13
|
+
Requires-Dist: json-repair<1,>=0.30
|
|
14
|
+
Requires-Dist: jsonschema<5,>=4.23
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest<9,>=8; extra == "test"
|
|
17
|
+
Requires-Dist: pytest-asyncio<1,>=0.23; extra == "test"
|
|
18
|
+
Requires-Dist: respx<1,>=0.21; extra == "test"
|
|
19
|
+
Requires-Dist: build>=1; extra == "test"
|
|
20
|
+
Requires-Dist: twine>=5; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# llm-mesh
|
|
24
|
+
|
|
25
|
+
Async Python clients for OpenAI-compatible APIs and GigaChat, with shared
|
|
26
|
+
request/response types, streaming, structured output, retries, and a model catalog.
|
|
27
|
+
|
|
28
|
+
Requires Python 3.11 or newer. Licensed under MIT.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Install from a checked-out repository:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
python -m pip install -e /path/to/llm-mesh
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or install a reviewed Git revision:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
python -m pip install "llm-mesh @ git+https://github.com/SergeyLizin/llm-mesh.git@<commit>"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The package is not yet published to PyPI.
|
|
45
|
+
|
|
46
|
+
## OpenAI-compatible APIs
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import asyncio
|
|
50
|
+
import os
|
|
51
|
+
|
|
52
|
+
from llm_mesh import LLMRequest, OpenAIClient
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
client = OpenAIClient(
|
|
57
|
+
model=os.environ["LLM_MODEL"],
|
|
58
|
+
base_url=os.environ["LLM_BASE_URL"],
|
|
59
|
+
api_key=os.environ["LLM_API_KEY"],
|
|
60
|
+
)
|
|
61
|
+
try:
|
|
62
|
+
response = await client.generate_text(
|
|
63
|
+
LLMRequest(system="Be concise.", user="Explain HTTP caching.", max_tokens=256)
|
|
64
|
+
)
|
|
65
|
+
print(response.text)
|
|
66
|
+
finally:
|
|
67
|
+
await client.aclose()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
asyncio.run(main())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`GigaChatAsyncClient` offers the same text, structured-output and streaming
|
|
74
|
+
interfaces, with GigaChat authentication. Batch helpers are available in
|
|
75
|
+
`llm_mesh.gigachat.batch`. `LLMUsage` normalizes provider token usage, including
|
|
76
|
+
cache and reasoning tokens.
|
|
77
|
+
|
|
78
|
+
## Environment configuration
|
|
79
|
+
|
|
80
|
+
Set environment variables before constructing a client. The library does not
|
|
81
|
+
load `.env` files automatically. In this section, **OpenAI** means any endpoint
|
|
82
|
+
used through `OpenAIClient`; **GigaChat** means `GigaChatAsyncClient`.
|
|
83
|
+
Provider-specific environment aliases are not inferred. A catalog route may
|
|
84
|
+
explicitly reference any secret variable through `api_key_env` (and similarly
|
|
85
|
+
`base_url_env`, `model_env`, `scope_env`, or `auth_url_env`).
|
|
86
|
+
|
|
87
|
+
### Catalog selection and connection
|
|
88
|
+
|
|
89
|
+
- `LLM_MODELS_CONFIG`: catalog JSON path. An explicit catalog path argument
|
|
90
|
+
takes precedence; when neither is set, the bundled `models.json` is used.
|
|
91
|
+
- `LLM_MODEL_ID`: catalog entry ID consumed by `make_selected_client()`.
|
|
92
|
+
Unset or empty means no selection and the function returns `None`.
|
|
93
|
+
- `LLM_PROVIDER`: route kind, `openai` or `gigachat`, exported by catalog
|
|
94
|
+
selection for callers that dispatch by provider. Setting it does not change
|
|
95
|
+
the class of a directly constructed client.
|
|
96
|
+
- `LLM_MODEL`: provider model name exported by catalog selection. The catalog
|
|
97
|
+
factory passes it to the client. Direct constructors use their `model`
|
|
98
|
+
argument; pass this variable explicitly, as in the example above.
|
|
99
|
+
- `LLM_PROVIDER_LABEL`: diagnostic provider label for OpenAI; defaults to
|
|
100
|
+
`openai` unless a constructor label is supplied. Catalog selection exports
|
|
101
|
+
the route's `provider` label, falling back to its kind.
|
|
102
|
+
- `LLM_BASE_URL`: API endpoint. Required for OpenAI unless `base_url` is passed.
|
|
103
|
+
GigaChat defaults to `https://gigachat.devices.sberbank.ru/api/v1`.
|
|
104
|
+
A Batch client also inherits the endpoint of its supplied authentication client.
|
|
105
|
+
- `LLM_API_KEY`: OpenAI API key, or GigaChat encoded OAuth client credentials.
|
|
106
|
+
No default. Explicit credentials take precedence. A GigaChat client may
|
|
107
|
+
instead receive an existing access token through the `token` argument.
|
|
108
|
+
- `LLM_AUTH_URL`: GigaChat OAuth token endpoint; defaults to
|
|
109
|
+
`https://ngw.devices.sberbank.ru:9443/api/v2/oauth`. Unused by OpenAI.
|
|
110
|
+
- `LLM_AUTH_SCOPE`: GigaChat OAuth scope, such as `GIGACHAT_API_PERS`,
|
|
111
|
+
`GIGACHAT_API_B2B`, or `GIGACHAT_API_CORP`; no default scope is configured.
|
|
112
|
+
Unused by OpenAI. These scope strings are protocol values, not environment aliases.
|
|
113
|
+
- `LLM_VERIFY_SSL`: TLS certificate verification. Defaults to `true` for
|
|
114
|
+
OpenAI and `false` for GigaChat and Batch. Case-insensitive `0`, `false`,
|
|
115
|
+
and `no` disable verification; other nonempty values enable it.
|
|
116
|
+
An explicit `verify` argument takes precedence.
|
|
117
|
+
|
|
118
|
+
### Options and precedence
|
|
119
|
+
|
|
120
|
+
`LLM_OPTIONS` is a JSON object containing request and runtime settings:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
export LLM_OPTIONS='{"max_output_tokens":8192,"max_concurrent":2,"extra_body":{"top_p":0.9}}'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
For every setting below, the JSON key is the lowercase environment name
|
|
127
|
+
without `LLM_`: for example, `LLM_HTTP_TIMEOUT` becomes `http_timeout`.
|
|
128
|
+
Use JSON booleans, numbers, and objects directly, without encoding objects as
|
|
129
|
+
strings. The connection and catalog selection variables above are read separately
|
|
130
|
+
and cannot be configured inside `LLM_OPTIONS`.
|
|
131
|
+
|
|
132
|
+
- Unset or empty `LLM_OPTIONS` lets clients read standalone variables below.
|
|
133
|
+
- A present JSON key overrides its standalone variable. JSON `null` selects
|
|
134
|
+
the client default. Invalid JSON or a non-object value raises `ValueError`.
|
|
135
|
+
- When `LLM_OPTIONS` is present, including `{}`, omitted **route options** use
|
|
136
|
+
client defaults and ignore old standalone values. Route options are all
|
|
137
|
+
settings in the next three subsections (output, reasoning, and schema/usage).
|
|
138
|
+
- Omitted **runtime options** in the transport/retry and Batch subsections
|
|
139
|
+
still fall back to standalone variables. Thus `{}` does not reset those
|
|
140
|
+
process-wide defaults.
|
|
141
|
+
|
|
142
|
+
Catalog selection manages only nine variables: the eight connection variables
|
|
143
|
+
from `LLM_PROVIDER` through `LLM_VERIFY_SSL`, plus `LLM_OPTIONS`.
|
|
144
|
+
`route_env()` builds the options object from route fields, mapping `max_tokens`
|
|
145
|
+
to `max_output_tokens`. It also copies `http_timeout`, `no_degrade`,
|
|
146
|
+
`length_retries`, and `length_retry_cap` when declared. For GigaChat,
|
|
147
|
+
`reasoning_on.reasoning_effort` supplies the default `reasoning_effort` option.
|
|
148
|
+
Other runtime controls can be set through standalone variables or a manually
|
|
149
|
+
constructed `LLM_OPTIONS` object.
|
|
150
|
+
|
|
151
|
+
`apply_route_env()` replaces all nine managed values, including empty values;
|
|
152
|
+
the CLI's `--env` output does the same with shell exports. Missing OAuth fields
|
|
153
|
+
are cleared, so declare `auth_url` or `auth_url_env` on a route that requires a
|
|
154
|
+
custom token endpoint. Catalog selection replaces an existing `LLM_OPTIONS`
|
|
155
|
+
object rather than merging it.
|
|
156
|
+
|
|
157
|
+
Explicit constructor overrides such as `verify`, `http_timeout`/`timeout_s`,
|
|
158
|
+
`reasoning_field`, and `no_degrade` take precedence where supported.
|
|
159
|
+
`length_retry_cap` is an exception: it supplies a default that the environment
|
|
160
|
+
can override. Boolean enable flags below accept case-insensitive `1`, `true`,
|
|
161
|
+
and `yes`; other values disable them. All durations are in seconds.
|
|
162
|
+
|
|
163
|
+
### Output and concurrency (route options)
|
|
164
|
+
|
|
165
|
+
- `LLM_MAX_OUTPUT_TOKENS`: positive integer output-budget ceiling for both
|
|
166
|
+
clients. OpenAI has no default ceiling. GigaChat uses model-family limits
|
|
167
|
+
by default: 4096 for base models, 8192 for Pro, 16384 for Max, and 32768 for
|
|
168
|
+
Ultra. `use_model_token_limits=False` disables these inferred limits, but
|
|
169
|
+
an explicit setting still applies.
|
|
170
|
+
- `LLM_MIN_OUTPUT_TOKENS`: positive integer floor for the requested output
|
|
171
|
+
budget, useful for reasoning models. OpenAI only; unset means no floor.
|
|
172
|
+
- `LLM_MAX_CONCURRENT`: positive integer limit on concurrent outbound
|
|
173
|
+
generation requests per client instance. Both clients; unset means no limit.
|
|
174
|
+
GigaChat's explicit `max_concurrent` argument takes precedence.
|
|
175
|
+
- `LLM_FORCE_TEMPERATURE`: numeric temperature override for every request.
|
|
176
|
+
Both clients; unset preserves the request's value.
|
|
177
|
+
- `LLM_FORCE_TOP_P`: numeric `top_p` override. GigaChat only; unset means no
|
|
178
|
+
override. OpenAI endpoints can receive `top_p` through `extra_body`.
|
|
179
|
+
|
|
180
|
+
### Reasoning (route options)
|
|
181
|
+
|
|
182
|
+
- `LLM_DISABLE_REASONING`: boolean, default `false`. OpenAI applies the declared
|
|
183
|
+
`reasoning_off` object and removes declared `reasoning_on` fields; without
|
|
184
|
+
a valid off object, the configured baseline is preserved. GigaChat suppresses
|
|
185
|
+
`reasoning_effort` and sends `chat_template_kwargs.enable_thinking=false`.
|
|
186
|
+
- `LLM_REASONING_ON`: JSON object declaring OpenAI request fields that enable
|
|
187
|
+
reasoning; default `{}`. Merged into the extra request body.
|
|
188
|
+
- `LLM_REASONING_OFF`: JSON object declaring OpenAI request fields that disable
|
|
189
|
+
reasoning; unset means no off dialect. An explicit `{}` removes declared on
|
|
190
|
+
fields without adding replacement fields.
|
|
191
|
+
- `LLM_DISABLE_THINKING_FOR_TOOLS`: boolean, default `false`. OpenAI only;
|
|
192
|
+
applies the declared off dialect to native function requests, preserving
|
|
193
|
+
reasoning for text generation and text fallback. Requires a valid off object.
|
|
194
|
+
- `LLM_REASONING_EFFORT`: GigaChat default reasoning effort: `low`, `medium`,
|
|
195
|
+
or `high`. Unset or invalid values add no effort setting. A request's
|
|
196
|
+
`reasoning_effort` takes precedence; disabling reasoning suppresses it.
|
|
197
|
+
- `LLM_REASONING_FIELD`: response field containing reasoning text. OpenAI
|
|
198
|
+
defaults to checking `reasoning_content` and `reasoning`; GigaChat defaults
|
|
199
|
+
to `reasoning_content`. An explicit `reasoning_field` argument takes precedence.
|
|
200
|
+
|
|
201
|
+
### Schema, request extensions, and usage (route options)
|
|
202
|
+
|
|
203
|
+
These settings apply to OpenAI clients only.
|
|
204
|
+
|
|
205
|
+
- `LLM_DISABLE_TOOLS`: boolean, default `false`. Selects text-based emulation
|
|
206
|
+
for structured output instead of native tool calling.
|
|
207
|
+
- `LLM_TOOL_CHOICE_PREF`: initial structured-output preference: `auto`, `required`, or
|
|
208
|
+
`text`. Unset uses automatic negotiation starting with strict named-function
|
|
209
|
+
selection. `auto` starts with automatic function selection before trying
|
|
210
|
+
forced forms. For multi-tool requests, `required` forces a tool call and the
|
|
211
|
+
default is `auto`. An explicit `tool_choice_pref` argument takes precedence.
|
|
212
|
+
- `LLM_RESPONSE_FORMAT`: declared native structured-output dialect:
|
|
213
|
+
`json_schema` or `json_object`. Unset disables this additional fallback tier.
|
|
214
|
+
`json_object` guarantees JSON syntax, while `json_schema` includes the schema
|
|
215
|
+
when the request provides one.
|
|
216
|
+
- `LLM_OPEN_OBJECT_SCHEMAS`: set to `unsupported` to bypass schema-bearing
|
|
217
|
+
tiers for schemas containing open objects. Unset leaves the client to learn
|
|
218
|
+
this restriction from matching provider errors.
|
|
219
|
+
- `LLM_SANITIZE_ENUMS`: boolean, default `false`. Normalizes enum literals to
|
|
220
|
+
strings for Google-compatible function schemas.
|
|
221
|
+
- `LLM_EXTRA_BODY`: JSON object of additional provider request fields; default
|
|
222
|
+
`{}`. Existing generated body fields generally take precedence; reasoning
|
|
223
|
+
on/off settings apply their declared overrides.
|
|
224
|
+
- `LLM_EXTRA_HEADERS`: JSON object of extra HTTP headers; default `{}`. Header
|
|
225
|
+
values are converted to strings, and matching constructor header keys are
|
|
226
|
+
overwritten by these settings.
|
|
227
|
+
- `LLM_CACHE_HIT_FIELD`: flat usage field for cache-hit tokens; defaults to
|
|
228
|
+
`prompt_cache_hit_tokens`.
|
|
229
|
+
- `LLM_CACHE_MISS_FIELD`: flat usage field for cache-miss tokens; defaults to
|
|
230
|
+
`prompt_cache_miss_tokens`.
|
|
231
|
+
- `LLM_CACHE_NESTED_FIELD`: dotted usage path for cache-hit tokens when flat
|
|
232
|
+
hits are absent; defaults to `prompt_tokens_details.cached_tokens`. Misses
|
|
233
|
+
are derived from prompt tokens when possible. Empty cache-field settings
|
|
234
|
+
select the defaults; unavailable cache statistics are reported as `-1`.
|
|
235
|
+
|
|
236
|
+
### Transport and retries (runtime options)
|
|
237
|
+
|
|
238
|
+
- `LLM_HTTP_TIMEOUT`: HTTP timeout, default `600` for OpenAI and GigaChat
|
|
239
|
+
generation, `120` for Batch HTTP calls. GigaChat generation uses a separate
|
|
240
|
+
30-second connect timeout. Explicit timeout arguments take precedence.
|
|
241
|
+
- `LLM_MAX_RETRIES`: additional OpenAI retries for retryable failures; default
|
|
242
|
+
`3` (up to four attempts). GigaChat transient retries use constructor settings.
|
|
243
|
+
- `LLM_RETRY_BACKOFF_S`: OpenAI exponential-backoff base delay; default `1.0`.
|
|
244
|
+
Retries add jitter and may honor a server's retry delay.
|
|
245
|
+
- `LLM_LENGTH_RETRIES`: nonnegative number of additional attempts after output
|
|
246
|
+
truncation, increasing the token budget. Defaults to `2` for OpenAI and `1`
|
|
247
|
+
for GigaChat; `0` disables these retries.
|
|
248
|
+
- `LLM_LENGTH_RETRY_CAP`: positive integer OpenAI token-budget ceiling for
|
|
249
|
+
length retries; default `32768`, or the constructor's `length_retry_cap`.
|
|
250
|
+
If the minimum budget reaches an implicit cap, the cap grows to twice that
|
|
251
|
+
minimum; an explicit environment cap is preserved. GigaChat instead uses
|
|
252
|
+
its configured model/output ceiling.
|
|
253
|
+
- `LLM_STREAM_TRANSPORT`: boolean, default `false`. OpenAI only; uses SSE
|
|
254
|
+
internally for non-streaming generation calls and reconstructs the final
|
|
255
|
+
response. The normal `generate_stream()` API does not require this flag.
|
|
256
|
+
- `LLM_NO_DEGRADE`: boolean, default `false`. OpenAI only; raises instead of
|
|
257
|
+
implicitly falling back from native structured output to text emulation.
|
|
258
|
+
Explicit text mode and negotiation between native tool forms remain allowed.
|
|
259
|
+
- `LLM_FREQUENCY_PENALTY`: optional numeric OpenAI `frequency_penalty` value.
|
|
260
|
+
Unset adds no value; an existing `extra_body` value takes precedence.
|
|
261
|
+
- `LLM_REPETITION_PENALTY`: optional numeric OpenAI-compatible
|
|
262
|
+
`repetition_penalty` value, with the same precedence. The endpoint must
|
|
263
|
+
support this field; unset adds no value.
|
|
264
|
+
|
|
265
|
+
### GigaChat Batch API (runtime options)
|
|
266
|
+
|
|
267
|
+
- `LLM_BATCH_MODE`: boolean, default `false`. Enables Batch generation in the
|
|
268
|
+
catalog factory for GigaChat routes. It does not change directly constructed
|
|
269
|
+
generation clients or enable Batch for OpenAI routes.
|
|
270
|
+
- `LLM_BATCH_POLL_INTERVAL_S`: interval between batch status polls; default `5.0`.
|
|
271
|
+
- `LLM_BATCH_MAX_WAIT_S`: maximum wait for batch completion; default `3600.0`.
|
|
272
|
+
- `LLM_BATCH_HTTP_RETRIES`: maximum number of HTTP attempts, including the
|
|
273
|
+
initial attempt; default `12`, minimum `1`. The budget covers rate-limit
|
|
274
|
+
retries and an authentication refresh attempt.
|
|
275
|
+
- `LLM_BATCH_429_BACKOFF_START`: initial delay after HTTP 429; default `1.0`.
|
|
276
|
+
Subsequent delays grow by a factor of 1.5, capped at 45 seconds.
|
|
277
|
+
- `LLM_BATCH_COALESCE_MAX`: maximum number of pending generation requests
|
|
278
|
+
grouped into one batch by `BatchingLLMClient`; default `16`, minimum `1`.
|
|
279
|
+
- `LLM_BATCH_COALESCE_DELAY_S`: coalescing delay before submitting a partial
|
|
280
|
+
group; default `0.25`.
|
|
281
|
+
|
|
282
|
+
Batch uses the connection/authentication settings above and `LLM_HTTP_TIMEOUT`.
|
|
283
|
+
Explicit Batch constructor settings override their environment defaults.
|
|
284
|
+
|
|
285
|
+
## Request policies and streaming events
|
|
286
|
+
|
|
287
|
+
Both clients use the same transport implementation for every caller. Configure
|
|
288
|
+
behavior explicitly instead of choosing a separate client implementation:
|
|
289
|
+
|
|
290
|
+
- `OpenAIClient(fallback_policy="preserve")` exposes corrupt arguments, schema
|
|
291
|
+
validation failures, open-object errors, and exhausted gateway failures without
|
|
292
|
+
generating a replacement response. Declared open-object restrictions do not
|
|
293
|
+
bypass the native request in this mode; the actual provider response is used.
|
|
294
|
+
A multi-tool text turn is returned as text rather than forced into another call.
|
|
295
|
+
Native `tool_choice` negotiation after an explicit unsupported-form error and
|
|
296
|
+
normal transport/length retries remain enabled. Implicit text fallback is
|
|
297
|
+
rejected; explicitly selected text emulation still works. The default policy
|
|
298
|
+
is `"recover"`. This constructor policy is independent of `LLM_NO_DEGRADE`.
|
|
299
|
+
- `OpenAIClient(validate_schema=False)` returns parsed structured arguments
|
|
300
|
+
without retrying schema violations, so callers can score the original model
|
|
301
|
+
response. The default is `True`; this policy covers tool and response-format
|
|
302
|
+
tiers. JSON parsing and transport error handling still apply.
|
|
303
|
+
- `OpenAIClient(no_degrade=True)` raises when native structured output would
|
|
304
|
+
otherwise fall back to text emulation. Explicit text mode remains available.
|
|
305
|
+
- `length_retry_cap` sets the default OpenAI output budget ceiling for length
|
|
306
|
+
retries; `LLM_LENGTH_RETRY_CAP` overrides it. `reasoning_field` selects the
|
|
307
|
+
response field to read and takes precedence over `LLM_REASONING_FIELD`.
|
|
308
|
+
- `tool_choice_pref="required"` selects the initial OpenAI tool-choice form.
|
|
309
|
+
`disable_thinking_for_tools=True` applies the declared reasoning-off dialect
|
|
310
|
+
only to native function requests.
|
|
311
|
+
- `GigaChatAsyncClient(tool_choice="auto")` sends the tools from `LLMRequest.tools`
|
|
312
|
+
using legacy automatic function selection. The default `"single"` forces
|
|
313
|
+
`function_name` with `schema_`. Native modern tool loops (`tools_required`)
|
|
314
|
+
are unsupported by the GigaChat client and raise a validation error.
|
|
315
|
+
- `GigaChatAsyncClient(use_model_token_limits=False)` leaves the output limit
|
|
316
|
+
to the caller unless `LLM_MAX_OUTPUT_TOKENS` supplies an explicit ceiling.
|
|
317
|
+
`LLMRequest(mode="json_schema", schema=...)` selects native JSON Schema output.
|
|
318
|
+
|
|
319
|
+
`LLM_REASONING_ON` and `LLM_REASONING_OFF` contain JSON objects describing an
|
|
320
|
+
OpenAI-compatible provider's request fields. Disabling reasoning uses only the declared off
|
|
321
|
+
object; an empty object removes declared on fields. If the off dialect is
|
|
322
|
+
missing or invalid, the client preserves the configured baseline and does not
|
|
323
|
+
invent vendor fields. Catalog entries expose these objects as `reasoning_on`
|
|
324
|
+
and `reasoning_off`.
|
|
325
|
+
|
|
326
|
+
Alongside `generate_stream()`, both clients expose `generate_stream_events()`.
|
|
327
|
+
It yields typed content, reasoning, tool, completion, and error events from
|
|
328
|
+
`llm_mesh.stream_events`; `Complete` includes available usage and finish reason.
|
|
329
|
+
A transport failure yields `Error` and then raises the corresponding exception.
|
|
330
|
+
`EventStreamGenerator` describes this interface for type checking.
|
|
331
|
+
|
|
332
|
+
For synchronous model discovery before constructing a client, use
|
|
333
|
+
`llm_mesh.list_models(base_url, api_key)`. It returns the endpoint's model IDs
|
|
334
|
+
in their advertised order; selecting a model belongs to the caller.
|
|
335
|
+
|
|
336
|
+
## Package layout
|
|
337
|
+
|
|
338
|
+
Provider code is grouped in `llm_mesh.openai` and `llm_mesh.gigachat`.
|
|
339
|
+
Each contains `client.py` and private `_common.py` helpers. OpenAI-compatible
|
|
340
|
+
model discovery lives in `openai/discovery.py`; the GigaChat Batch API lives
|
|
341
|
+
in `gigachat/batch.py`. Request and response types, retries, SSE parsing,
|
|
342
|
+
the built-in canary, optional hooks, and the public model catalog remain
|
|
343
|
+
shared at the package root.
|
|
344
|
+
|
|
345
|
+
Import clients from `llm_mesh`, `llm_mesh.openai`, or `llm_mesh.gigachat`.
|
|
346
|
+
Provider subpackages also export `list_models` and the GigaChat batch clients,
|
|
347
|
+
respectively.
|
|
348
|
+
|
|
349
|
+
## Model catalog
|
|
350
|
+
|
|
351
|
+
The library ships a shared public `models.json` with models and ordered provider
|
|
352
|
+
routes. A project can keep its additional models in its own `models.json`, without
|
|
353
|
+
copying or modifying the public catalog. Credentials are supplied through the
|
|
354
|
+
environment variables named by the routes. Loading a catalog makes no API calls.
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from llm_mesh.models_catalog import load_catalog, resolve_route, make_client
|
|
358
|
+
|
|
359
|
+
routes = load_catalog()
|
|
360
|
+
# Choose an id@provider present in the catalog and configure its credentials:
|
|
361
|
+
route = resolve_route("your-model-id@your-provider", routes)
|
|
362
|
+
client = make_client(route)
|
|
363
|
+
# Use the client and await client.aclose() when finished.
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
To add project models, copy [examples/models.json](examples/models.json) into your
|
|
367
|
+
project and edit its entries. `extends` includes all models from the public catalog:
|
|
368
|
+
|
|
369
|
+
```json
|
|
370
|
+
{
|
|
371
|
+
"extends": "llm-mesh",
|
|
372
|
+
"models": [
|
|
373
|
+
{
|
|
374
|
+
"id": "custom-model",
|
|
375
|
+
"kind": "openai",
|
|
376
|
+
"providers": [{
|
|
377
|
+
"name": "custom",
|
|
378
|
+
"model_env": "CUSTOM_LLM_MODEL",
|
|
379
|
+
"base_url_env": "CUSTOM_LLM_BASE_URL",
|
|
380
|
+
"api_key_env": "CUSTOM_LLM_API_KEY"
|
|
381
|
+
}]
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Load both catalogs together by specifying the project file:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
routes = load_catalog("/path/to/your/project/models.json")
|
|
391
|
+
# Public model ids and custom-model@custom are now available in routes.
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Alternatively, set `LLM_MODELS_CONFIG=/path/to/your/project/models.json` and call
|
|
395
|
+
`load_catalog()` without arguments. An explicit path takes precedence over this
|
|
396
|
+
variable. With neither, only the bundled catalog is loaded. Files in the working
|
|
397
|
+
directory are never picked up implicitly.
|
|
398
|
+
|
|
399
|
+
New ids are appended to the public catalog. If an id already exists, the project
|
|
400
|
+
entry replaces that complete model, including its provider list. Public catalog
|
|
401
|
+
updates remain available unless the project overrides the same id. Optional
|
|
402
|
+
`exclude` removes ids. `order` lists unique known ids to place first; unlisted
|
|
403
|
+
ids follow in catalog order, so newly added public models remain available.
|
|
404
|
+
Use `patches` to change individual fields without freezing the entire model:
|
|
405
|
+
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"extends": "llm-mesh",
|
|
409
|
+
"patches": [
|
|
410
|
+
{
|
|
411
|
+
"id": "existing-model-id",
|
|
412
|
+
"providers": [{"name": "existing-provider", "max_concurrent": 2}],
|
|
413
|
+
"exclude_providers": ["unwanted-provider"]
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Patches apply after `models` and before `exclude`/`order`. Each patch must name
|
|
420
|
+
an existing model id. Provider changes match by `name`; new names append routes,
|
|
421
|
+
while unmentioned routes and fields keep inheriting public updates. Model and
|
|
422
|
+
provider fields are shallow overrides: an object such as `extra_body` replaces
|
|
423
|
+
that whole field, and `null` removes a field. Use `exclude_providers` to remove
|
|
424
|
+
routes explicitly. Duplicate patch ids/names are rejected. Full replacements
|
|
425
|
+
through `models` remain available when intentional isolation is required.
|
|
426
|
+
|
|
427
|
+
Provider order within a model is preserved. For a fully independent catalog,
|
|
428
|
+
a plain JSON array is also supported and replaces the public catalog. Use
|
|
429
|
+
`load_catalog_data()` for nested entries, `load_catalog()` for flattened routes,
|
|
430
|
+
and `catalog_digest()` for an effective catalog fingerprint.
|
|
431
|
+
|
|
432
|
+
`make_client()` uses the legacy environment-based configuration and updates
|
|
433
|
+
process-wide variables. Construct catalog clients serially, before issuing
|
|
434
|
+
requests. For independent configuration use explicit client constructors.
|
|
435
|
+
|
|
436
|
+
## Canary checks
|
|
437
|
+
|
|
438
|
+
Set a session marker with `llm_mesh.canary.set_canary_context_token()`. Clients
|
|
439
|
+
append it to the system prompt and scan responses. Reset the token in a
|
|
440
|
+
`finally` block so it cannot leak into later calls.
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
from llm_mesh.canary import reset_canary_context_token, set_canary_context_token
|
|
444
|
+
|
|
445
|
+
token = set_canary_context_token("session-marker")
|
|
446
|
+
try:
|
|
447
|
+
...
|
|
448
|
+
finally:
|
|
449
|
+
reset_canary_context_token(token)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
A different prompt or detector can replace the defaults through
|
|
453
|
+
`llm_mesh.hooks.configure_canary_hooks()`.
|
|
454
|
+
|
|
455
|
+
## Development
|
|
456
|
+
|
|
457
|
+
Write all documentation, comments, catalog notes, built-in prompts, log messages,
|
|
458
|
+
errors, and test fixtures in English. Caller-provided text and provider responses
|
|
459
|
+
retain their original language; the library does not translate them.
|
|
460
|
+
Keep descriptions and examples self-contained, without references to private
|
|
461
|
+
repositories, internal issue trackers, or the library's extraction history.
|
|
462
|
+
|
|
463
|
+
```sh
|
|
464
|
+
python -m venv .venv
|
|
465
|
+
.venv/bin/python -m pip install -e '.[test]'
|
|
466
|
+
.venv/bin/python -m pytest -q
|
|
467
|
+
.venv/bin/python -m build
|
|
468
|
+
.venv/bin/python -m twine check dist/*
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Tests use mocked HTTP transports and do not require provider credentials.
|
|
472
|
+
|
|
473
|
+
## Releases
|
|
474
|
+
|
|
475
|
+
`.github/workflows/release.yml` runs on tags such as `v1.0.0`. It runs the test
|
|
476
|
+
matrix, checks that the tag matches `project.version` in `pyproject.toml`, builds
|
|
477
|
+
the wheel and source distribution, and checks an installed wheel outside the
|
|
478
|
+
checkout. The same distribution artifacts are then published to PyPI and
|
|
479
|
+
attached to a GitHub Release with generated release notes.
|
|
480
|
+
|
|
481
|
+
Before the first release, create the GitHub environment `pypi` and configure a
|
|
482
|
+
[PyPI Trusted Publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
|
|
483
|
+
(or a pending publisher for a new project) with these exact settings:
|
|
484
|
+
|
|
485
|
+
- Project: `llm-mesh`.
|
|
486
|
+
- GitHub owner: `SergeyLizin`.
|
|
487
|
+
- Repository: `llm-mesh`.
|
|
488
|
+
- Workflow filename: `release.yml`.
|
|
489
|
+
- Environment: `pypi`.
|
|
490
|
+
|
|
491
|
+
Publishing uses GitHub OIDC; no PyPI API token secret is needed. Restrict the
|
|
492
|
+
`pypi` environment to release tags (`v*`). The workflow files do not create the
|
|
493
|
+
PyPI publisher or configure the GitHub environment automatically.
|
|
494
|
+
|
|
495
|
+
For a release, update `project.version`, commit the final contents, then push
|
|
496
|
+
the matching tag. For example, once version `1.0.0` is ready:
|
|
497
|
+
|
|
498
|
+
```sh
|
|
499
|
+
git tag -a v1.0.0 -m "Release 1.0.0"
|
|
500
|
+
git push origin main
|
|
501
|
+
git push origin v1.0.0
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
A manual **Run workflow** invocation only tests and builds downloadable
|
|
505
|
+
artifacts, even when a tag is selected; it never publishes. Use it to rehearse
|
|
506
|
+
the pipeline. Once a version is published to PyPI, release changes under a new
|
|
507
|
+
version. If PyPI succeeds but GitHub Release creation fails, rerun only the
|
|
508
|
+
failed job to reuse the already-built artifacts without republishing to PyPI.
|