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.
Files changed (58) hide show
  1. llm_mesh-1.0.0/LICENSE +21 -0
  2. llm_mesh-1.0.0/MANIFEST.in +1 -0
  3. llm_mesh-1.0.0/PKG-INFO +508 -0
  4. llm_mesh-1.0.0/README.md +486 -0
  5. llm_mesh-1.0.0/examples/models.json +18 -0
  6. llm_mesh-1.0.0/pyproject.toml +33 -0
  7. llm_mesh-1.0.0/setup.cfg +4 -0
  8. llm_mesh-1.0.0/src/llm_mesh/__init__.py +48 -0
  9. llm_mesh-1.0.0/src/llm_mesh/_common.py +193 -0
  10. llm_mesh-1.0.0/src/llm_mesh/_retry.py +52 -0
  11. llm_mesh-1.0.0/src/llm_mesh/_streaming.py +206 -0
  12. llm_mesh-1.0.0/src/llm_mesh/canary.py +67 -0
  13. llm_mesh-1.0.0/src/llm_mesh/config.py +82 -0
  14. llm_mesh-1.0.0/src/llm_mesh/gigachat/__init__.py +9 -0
  15. llm_mesh-1.0.0/src/llm_mesh/gigachat/_common.py +131 -0
  16. llm_mesh-1.0.0/src/llm_mesh/gigachat/batch.py +542 -0
  17. llm_mesh-1.0.0/src/llm_mesh/gigachat/client.py +1311 -0
  18. llm_mesh-1.0.0/src/llm_mesh/hooks.py +40 -0
  19. llm_mesh-1.0.0/src/llm_mesh/models.json +3013 -0
  20. llm_mesh-1.0.0/src/llm_mesh/models_catalog.py +421 -0
  21. llm_mesh-1.0.0/src/llm_mesh/openai/__init__.py +6 -0
  22. llm_mesh-1.0.0/src/llm_mesh/openai/_common.py +132 -0
  23. llm_mesh-1.0.0/src/llm_mesh/openai/client.py +2144 -0
  24. llm_mesh-1.0.0/src/llm_mesh/openai/discovery.py +39 -0
  25. llm_mesh-1.0.0/src/llm_mesh/protocol.py +64 -0
  26. llm_mesh-1.0.0/src/llm_mesh/py.typed +0 -0
  27. llm_mesh-1.0.0/src/llm_mesh/stream_events.py +118 -0
  28. llm_mesh-1.0.0/src/llm_mesh/text_parsing.py +295 -0
  29. llm_mesh-1.0.0/src/llm_mesh/types.py +234 -0
  30. llm_mesh-1.0.0/src/llm_mesh.egg-info/PKG-INFO +508 -0
  31. llm_mesh-1.0.0/src/llm_mesh.egg-info/SOURCES.txt +56 -0
  32. llm_mesh-1.0.0/src/llm_mesh.egg-info/dependency_links.txt +1 -0
  33. llm_mesh-1.0.0/src/llm_mesh.egg-info/entry_points.txt +2 -0
  34. llm_mesh-1.0.0/src/llm_mesh.egg-info/requires.txt +11 -0
  35. llm_mesh-1.0.0/src/llm_mesh.egg-info/top_level.txt +1 -0
  36. llm_mesh-1.0.0/tests/test_canary.py +42 -0
  37. llm_mesh-1.0.0/tests/test_catalog_packaging.py +162 -0
  38. llm_mesh-1.0.0/tests/test_client_options.py +99 -0
  39. llm_mesh-1.0.0/tests/test_client_policy_regressions.py +78 -0
  40. llm_mesh-1.0.0/tests/test_client_transport_contracts.py +124 -0
  41. llm_mesh-1.0.0/tests/test_discovery.py +33 -0
  42. llm_mesh-1.0.0/tests/test_environment_config.py +120 -0
  43. llm_mesh-1.0.0/tests/test_extract_json_array_from_text.py +58 -0
  44. llm_mesh-1.0.0/tests/test_gigachat_batch.py +332 -0
  45. llm_mesh-1.0.0/tests/test_gigachat_structured_modes.py +492 -0
  46. llm_mesh-1.0.0/tests/test_hooks.py +25 -0
  47. llm_mesh-1.0.0/tests/test_llm_gigachat.py +1360 -0
  48. llm_mesh-1.0.0/tests/test_llm_openai.py +2724 -0
  49. llm_mesh-1.0.0/tests/test_llm_protocol.py +130 -0
  50. llm_mesh-1.0.0/tests/test_llm_truncation_warning_clipped.py +78 -0
  51. llm_mesh-1.0.0/tests/test_openai_gateway_5xx_tool_degrade.py +188 -0
  52. llm_mesh-1.0.0/tests/test_openai_multitool_corrupted_args.py +258 -0
  53. llm_mesh-1.0.0/tests/test_openai_open_object_schema.py +639 -0
  54. llm_mesh-1.0.0/tests/test_openai_reasoning_parity.py +41 -0
  55. llm_mesh-1.0.0/tests/test_preserve_fallback.py +108 -0
  56. llm_mesh-1.0.0/tests/test_stream_events.py +545 -0
  57. llm_mesh-1.0.0/tests/test_structured_schema_degradation.py +126 -0
  58. 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
@@ -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.