langchaint 0.3.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.
- langchaint-0.3.0/LICENSE +21 -0
- langchaint-0.3.0/PKG-INFO +167 -0
- langchaint-0.3.0/README.md +139 -0
- langchaint-0.3.0/pyproject.toml +203 -0
- langchaint-0.3.0/src/langchaint/__init__.py +98 -0
- langchaint-0.3.0/src/langchaint/anthropic/__init__.py +230 -0
- langchaint-0.3.0/src/langchaint/anthropic/messages_provider.py +856 -0
- langchaint-0.3.0/src/langchaint/exceptions.py +328 -0
- langchaint-0.3.0/src/langchaint/inference_params.py +30 -0
- langchaint-0.3.0/src/langchaint/llm.py +563 -0
- langchaint-0.3.0/src/langchaint/messages.py +218 -0
- langchaint-0.3.0/src/langchaint/openai/__init__.py +133 -0
- langchaint-0.3.0/src/langchaint/openai/responses_provider.py +792 -0
- langchaint-0.3.0/src/langchaint/provider.py +284 -0
- langchaint-0.3.0/src/langchaint/py.typed +0 -0
- langchaint-0.3.0/src/langchaint/rate_limiter.py +219 -0
- langchaint-0.3.0/src/langchaint/response.py +124 -0
- langchaint-0.3.0/src/langchaint/streaming.py +319 -0
- langchaint-0.3.0/src/langchaint/tools.py +302 -0
- langchaint-0.3.0/src/langchaint/tracing/__init__.py +668 -0
- langchaint-0.3.0/src/langchaint/usage.py +62 -0
langchaint-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Hopfensperger
|
|
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,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchaint
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: It ain't langchain.
|
|
5
|
+
Keywords: llm,anthropic,openai,claude,async
|
|
6
|
+
Author: John Hopfensperger
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Dist: pydantic>=2.13.4
|
|
14
|
+
Requires-Dist: anthropic>=0.116.0 ; extra == 'all'
|
|
15
|
+
Requires-Dist: openai>=2.45.0 ; extra == 'all'
|
|
16
|
+
Requires-Dist: anthropic>=0.116.0 ; extra == 'anthropic'
|
|
17
|
+
Requires-Dist: openai>=2.45.0 ; extra == 'openai'
|
|
18
|
+
Requires-Dist: opentelemetry-api>=1.30.0 ; extra == 'otel'
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Project-URL: Homepage, https://github.com/s-banach/langchaint
|
|
21
|
+
Project-URL: Repository, https://github.com/s-banach/langchaint
|
|
22
|
+
Project-URL: Issues, https://github.com/s-banach/langchaint/issues
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Provides-Extra: anthropic
|
|
25
|
+
Provides-Extra: openai
|
|
26
|
+
Provides-Extra: otel
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# langchaint
|
|
30
|
+
|
|
31
|
+
Provider-neutral async LLM client over the official provider SDKs (anthropic >= 0.116.0, openai >= 2.45.0).
|
|
32
|
+
Alpha: the API is unstable and may change without notice.
|
|
33
|
+
|
|
34
|
+
## Design
|
|
35
|
+
|
|
36
|
+
**Generation only via binding.**
|
|
37
|
+
`LLM` has no generate methods.
|
|
38
|
+
`LLM.bind(...) -> BoundLLM[OutputT]` freezes everything that determines the cacheable prompt prefix: `system_prompt`, `tool_manager`, `inference_params`, `tool_choice`, `parallel_tool_calls`, `automatic_prompt_caching`.
|
|
39
|
+
`response_format` is frozen too, as the field that fixes the output type.
|
|
40
|
+
`automatic_prompt_caching` has no default: caching changes billing, so every `bind` states it.
|
|
41
|
+
A `TextPart` or `ImagePart` with `cache_breakpoint=True` places a prompt-cache boundary at exactly that part, under either `automatic_prompt_caching` value, so binding `False` and marking parts is the fully user-specified caching configuration.
|
|
42
|
+
`generate_many(conversations, warm_cache=True)` runs the first conversation to completion before admitting the rest, so a batch sharing a cached prefix pays one cache write instead of one per in-flight item; it is opt-in because it costs one item of serial latency.
|
|
43
|
+
`system_prompt` also binds as a sequence of `TextPart`s, so a boundary can sit inside the frozen prefix (stable instructions marked, semi-stable context after).
|
|
44
|
+
There are no per-call parameter overrides; changing parameters is `rebind(...)`, which returns a new `BoundLLM` with the SDK keyword arguments converted again.
|
|
45
|
+
`bind(response_format=Model)` returns `BoundLLM[Model]`; without `response_format` it returns `BoundLLM[str]`, selected by overload.
|
|
46
|
+
`rebind` carries the same overload, so `rebind(response_format=Model)` switches the output type to `BoundLLM[Model]`, `rebind(response_format=None)` switches it back to `BoundLLM[str]`, and leaving it out keeps the current type.
|
|
47
|
+
Every generate and stream method takes a conversation of `Message`s; a bare `str` is accepted as shorthand for a conversation of one `UserMessage` holding that text.
|
|
48
|
+
|
|
49
|
+
**The catalog is a constructor function per backend returning a ready LLM (anthropic adds a second for Bedrock).**
|
|
50
|
+
`from langchaint.openai import openai_model` then `openai_model("gpt-5.6-terra")`, and `from langchaint.anthropic import anthropic_model` then `anthropic_model("claude-sonnet-5")`, take the provider's own model identifier (a `Literal`, so typos fail the type check), look up the public prices, construct the adapter, and wrap it in an `LLM`.
|
|
51
|
+
`client=None` constructs the native first-party SDK client from environment credentials.
|
|
52
|
+
Anthropic Bedrock is a second sibling constructor, `anthropic_bedrock_model(model, aws_region=...)`: it names the same catalog model and reads the model's Bedrock surface (which of two SDK client classes) and wire model id from a table, so the application names neither the client class nor the Bedrock id.
|
|
53
|
+
Constructing an adapter directly covers models outside the catalog.
|
|
54
|
+
|
|
55
|
+
**The SDKs are optional dependencies, one extra per backend.**
|
|
56
|
+
The neutral core (`LLM`, the message tree, the error taxonomy) imports no SDK, so `import langchaint` needs neither package.
|
|
57
|
+
Each backend lives in its own subpackage that imports its SDK at module top: install `langchaint[openai]` or `langchaint[anthropic]` (or `langchaint[all]`) for the ones you use, and importing `langchaint.openai` without the openai package raises a `ModuleNotFoundError` naming the extra to install.
|
|
58
|
+
The import path is the boundary: only code that reaches for a backend requires its SDK, and a type checker following `langchaint.openai` never resolves anthropic's types.
|
|
59
|
+
OTel tracing follows the same pattern: `langchaint.tracing` imports only opentelemetry-api, install `langchaint[otel]`, and it stays off `import langchaint`.
|
|
60
|
+
|
|
61
|
+
**Adapters wrap SDK clients and delegate to the SDK.**
|
|
62
|
+
`AnthropicMessagesProvider(client=AsyncAnthropic(...))` and `OpenAIResponsesProvider(client=AsyncOpenAI(...))` call `messages.create/parse/stream` and `responses.create/parse/stream`; stream assembly and structured-output parsing are the SDK's, not hand-written.
|
|
63
|
+
Adapters store a `with_options(max_retries=0)` copy of the client, so the SDK never retries beneath the package's retry loop.
|
|
64
|
+
OpenAI support is the Responses API only: every supported OpenAI model speaks it, so there is no Chat Completions adapter.
|
|
65
|
+
The adapter always sends `store=False` because conversation state is the caller's conversation argument.
|
|
66
|
+
Anthropic Bedrock is two distinct SDK surfaces, the legacy `InvokeModel` client `AsyncAnthropicBedrock` and the Messages-API client `AsyncAnthropicBedrockMantle`, and the catalog models split across them, so `anthropic_bedrock_model` reads the client class and wire model id per model from `ANTHROPIC_BEDROCK`.
|
|
67
|
+
OpenAI on Bedrock is the bundled `AsyncBedrockOpenAI` passed as `client`; there is no Converse adapter.
|
|
68
|
+
Requests travel as typed frozen dataclasses whose optional fields are `X | Omit`, passed to the SDK as explicit keywords: no `**kwargs`, no hand-written wire TypedDicts, and the SDK overloads resolve without casts.
|
|
69
|
+
|
|
70
|
+
**Anthropic prompt caching is placed by the adapter.**
|
|
71
|
+
A bind-time `cache_control` marker goes on the system block (or the last tool when there is no system prompt), and a per-request marker goes on the last block of the last message, so the breakpoint follows a growing conversation.
|
|
72
|
+
Placement is manual because `messages.parse` lacks the top-level `cache_control` parameter; manual placement keeps create/parse/stream uniform.
|
|
73
|
+
Every marker's TTL is the adapter's `cache_ttl` (`"5m"` default, `"1h"` for entries that must survive longer gaps at 2x write cost), a keyword of `anthropic_model` and `anthropic_bedrock_model`.
|
|
74
|
+
|
|
75
|
+
**Usage counters partition, cost is the adapter's job.**
|
|
76
|
+
`Usage` stores `input_tokens_cache_read`, `input_tokens_cache_write`, `input_tokens_cache_none`, and `output_tokens`; the three input counters are a disjoint partition and `input_tokens_total` is derived.
|
|
77
|
+
`input_tokens_total_provider_reported` holds what the provider itself reported: openai's `input_tokens` includes cached and cache-write tokens, so the adapter fills it and a validator cross-checks the partition; anthropic reports no all-inclusive total, so it stays None.
|
|
78
|
+
The counters are validated non-negative, which is what guards the openai path: there the partition is derived by subtraction and sums to the reported total by construction.
|
|
79
|
+
`cost_in_usd` is computed inside the adapter from raw provider counts against a `PricingTable`, because providers split counters the normalized `Usage` collapses: anthropic bills 5-minute and 1-hour cache writes at different rates, and an adapter that sees 1-hour writes without `cache_write_1h_usd_per_million_tokens` raises `AbortBatchError` rather than misbill.
|
|
80
|
+
|
|
81
|
+
**Success is a `Response`, failure is a `GenerationError`.**
|
|
82
|
+
A generate that succeeds returns a frozen `Response[OutputT]` with every field present; one that ends terminally raises (or, in a batch, returns) a `GenerationError`.
|
|
83
|
+
Its three leaves are the terminal per-item outcomes: `RetriesExhaustedError` (transient budget spent), `RefusalError` (the model refused on the structured path), and `ExceededMaxCompletionTokensError` (the structured response hit the token cap before its JSON parsed).
|
|
84
|
+
On the base, `stop_reason` is `StopReason | None`: `"refusal"` or `"max_tokens"` on those two leaves, `None` on `RetriesExhaustedError`, whose attempts never reached a completed turn.
|
|
85
|
+
Both hold `attempt_records`, one `AttemptRecord` per request sent: raw `time.monotonic()` start and end readings bracketing the send only (slot waits and backoff sleeps excluded, so rate limiting is distinguishable from slow requests), the attempt's `TransientError` (None on the attempt that succeeded or a rejected 200), and the attempt's `usage`/`cost_in_usd` (None for a transport failure that billed nothing).
|
|
86
|
+
On a `Response` every record but the last failed; on a `GenerationError` the records describe the terminal outcome.
|
|
87
|
+
`attempts` is derived from the records on both.
|
|
88
|
+
Both also carry `model`, `provider_name`, and `elapsed_seconds` (first request to completion, waits included, so stored rather than derived), so the module-level `to_row(result)` flattens either to the same scalar keys and a mixed list of successes and failures is one table.
|
|
89
|
+
A `GenerationError` derives `usage`/`cost_in_usd` from its records, so a refusal or truncation reports its real cost while a retry-exhausted item whose attempts billed nothing reports zero; `error_text` carries the failure reason.
|
|
90
|
+
On a `Response`, `usage`, `cost_in_usd` (the successful attempt's own), `stop_reason`, `assistant_message`, and `raw` are always present.
|
|
91
|
+
`raw` is the SDK's own response model held by reference; it is never dumped to a dict, because dumping deep-copies every response body per request.
|
|
92
|
+
|
|
93
|
+
**One RateLimiter owns retrying and pacing.**
|
|
94
|
+
`RateLimiter` holds `max_attempts`, `backoff_base_seconds`, `backoff_max_seconds`, and `max_in_flight`; it is stateful and shareable, so one instance passed to several `LLM`s is one shared budget for the account they hit.
|
|
95
|
+
Its slot gates every request start on every path (first attempts, retries, batch items, stream openings); backoff sleeps outside the slot.
|
|
96
|
+
There is deliberately no `requests_per_minute`: an in-flight bound self-adjusts throughput along request duration, while a client-side rate number models one dimension of the provider's multi-dimensional limit and goes stale with the account tier.
|
|
97
|
+
A rate-limit error (`Provider.classify` returning `"rate_limit"`, or any error naming a server-stated retry-after) pauses admission for everyone sharing the limiter: for the server-stated wait when one was sent (parsed from the `retry-after-ms` / `retry-after` headers onto `TransientError.retry_after_seconds`, capped at 60 seconds), else for the failing task's backoff delay.
|
|
98
|
+
After the pause, admission stays limited to one probe request at a time until the probe succeeds; other transient errors (timeouts, 5xx) pause nobody, because they say nothing about the account's quota.
|
|
99
|
+
|
|
100
|
+
**Retry stays in the package, classification in the adapter.**
|
|
101
|
+
Adapters raise `TransientError`, `AbortBatchError`, or a `GenerationError` leaf directly where they know the answer: rate limits and overload (transient), a bad request (abort), and an empty structured parse, which splits three ways (a refusal is `RefusalError`, a token-cap truncation is `ExceededMaxCompletionTokensError`, anything else is `TransientError`).
|
|
102
|
+
The retry loop retries only `TransientError`; it honors the rest without asking.
|
|
103
|
+
Unrecognized exceptions go through `Provider.classify`, which returns `"rate_limit"`, `"transient"`, or `"abort"`, and anything it does not recognize is abort.
|
|
104
|
+
`generate_one` raises `RetriesExhaustedError` for transient exhaustion, `RefusalError` or `ExceededMaxCompletionTokensError` on the structured path, and `AbortBatchError` for an abort classification; the first three share the `GenerationError` base a caller can catch at once.
|
|
105
|
+
`generate_many` returns `list[Response[OutputT] | GenerationError]`, so a terminal per-item failure is a row in its slot rather than a raise, while an `AbortBatchError` still cancels the siblings and raises; results stay order-aligned.
|
|
106
|
+
|
|
107
|
+
**Streaming is a handle.**
|
|
108
|
+
`stream_one` returns a `StreamHandle`: an async iterator of `StreamItem = str | ToolCall`, an idempotent `await handle.final()` returning the assembled `Response`, and an async context manager so abandoning a stream closes the connection.
|
|
109
|
+
Text chunks are the provider SDK's own strings passed through without a wrapper class or copy, and each `ToolCall` is yielded once, complete, when its block closes.
|
|
110
|
+
There are deliberately no tool-call delta items (a consumer cannot act on partial argument JSON, and both SDKs accumulate the arguments and hand over the finished call) and no usage or stop items (usage, `cost_in_usd`, and `stop_reason` live on `final()`'s `Response`, and a bare-`str` stop reason could not share a union with bare-`str` text chunks).
|
|
111
|
+
Connection failures before the first yielded item retry under the `RateLimiter`; after the first yielded item nothing retries, because replaying chunks the caller already consumed would duplicate output.
|
|
112
|
+
An open stream holds one `RateLimiter` slot from opening until it closes or exhausts, so long-lived streams count against `max_in_flight`.
|
|
113
|
+
|
|
114
|
+
**Tools are explicit, dispatch is owned.**
|
|
115
|
+
A `Tool` is an async function taking one pydantic model and returning str or a sequence of content parts (text and images the model then sees), plus an explicit name and description; the args model is the schema source, so there is no signature introspection and no docstring scraping.
|
|
116
|
+
Tool content is model-facing, so it is exactly `MessageContent` (`str | Sequence[Part]`, the model-facing message body, aliased in `messages.py`): a function with a typed result serializes it to that form itself.
|
|
117
|
+
A function may instead return a `ToolOutputExplicit` wrapping that content plus `is_error` and `app_data` (the app-facing channel, which does carry a typed `BaseModel` or `Mapping` the model never sees).
|
|
118
|
+
`Tool.validate_and_run` validates raw call JSON against `args_model` and runs the function; it lives on `Tool` because there the args type parameter is concrete, so the validated arguments reach the function fully typed.
|
|
119
|
+
`Tool.dispatch` returns `DispatchHandled[AppDataT] | DispatchInvalidToolArgs`, so on the handled outcome a caller that dispatched a known tool reads `app_data` back at its concrete type with no `isinstance`.
|
|
120
|
+
`ToolManager` serializes the tools provider-neutrally, resolves the called name, and returns each outcome as a `DispatchOutcome`, a three-arm union over a heterogeneous tool set (where `app_data` erases to `BaseModel | Mapping[str, object] | None`): `DispatchHandled` carries the model-facing `ToolMessage` the caller appends to the conversation, paired with the function's `app_data` (data the model never sees); `DispatchInvalidToolArgs` carries a default `is_error` `ToolMessage` plus the pydantic `ValidationError` as a required field, so a caller that authors its own reply reads `validation_error.errors()` with no narrowing crutch; `DispatchUnknownTool` carries a default `is_error` `ToolMessage` naming the held tools plus the off-list `called_name`.
|
|
121
|
+
Every arm carries `tool_message`, so a caller that only appends the reply reads `result.tool_message` with no `match`.
|
|
122
|
+
An off-list tool name becomes a `DispatchUnknownTool` outcome rather than a raise, just like argument JSON the model got wrong becomes a `DispatchInvalidToolArgs` outcome: both are model data the model can correct (a provider can emit a name outside the sent schemas, and a rebind can strand an earlier turn's `tool_call`).
|
|
123
|
+
`tool_choice` is the neutral vocabulary `"auto" | "required" | SpecificTool | "none"` (anthropic's `"any"` maps from `"required"`); `parallel_tool_calls: bool` maps to anthropic `disable_parallel_tool_use` and openai `parallel_tool_calls`.
|
|
124
|
+
|
|
125
|
+
**The ReAct loop is a recipe, not vendored code.**
|
|
126
|
+
There is no agent class.
|
|
127
|
+
The loop is ~15 lines and is correct because the hard parts (retries, pacing, classification, validation) live below `generate_one` and `dispatch`, so a hand-copied loop cannot be subtly wrong.
|
|
128
|
+
The non-streaming loop runs over `generate_one`; the streaming loop runs over `stream_one`, printing text chunks as they arrive and dispatching the completed `ToolCall`s between turns, reading `dispatch(call).tool_message` for the ToolMessage it appends and matching `DispatchHandled` for the function's model-invisible `app_data` or `DispatchInvalidToolArgs` for the `validation_error`.
|
|
129
|
+
Owning the loop is what lets a caller enforce a budget mid-run, stream tokens, or swap the binding on a tool result, none of which a fixed agent surface exposes.
|
|
130
|
+
|
|
131
|
+
## Layout
|
|
132
|
+
|
|
133
|
+
CLAUDE.md design, naming, and review rules
|
|
134
|
+
README.md this file
|
|
135
|
+
src/langchaint/
|
|
136
|
+
messages.py message and part models, StopReason
|
|
137
|
+
usage.py Usage counters
|
|
138
|
+
inference_params.py InferenceParams
|
|
139
|
+
exceptions.py error vocabulary: TransientError,
|
|
140
|
+
AbortBatchError, AttemptRecord,
|
|
141
|
+
GenerationError and its leaves
|
|
142
|
+
response.py Response[OutputT], to_row
|
|
143
|
+
rate_limiter.py RateLimiter
|
|
144
|
+
tools.py Tool, ToolSchema, ToolManager
|
|
145
|
+
provider.py Binding, ToolChoice, PricingTable,
|
|
146
|
+
StreamItem, ProviderResult,
|
|
147
|
+
ProviderStream, BoundProvider, Provider
|
|
148
|
+
streaming.py StreamHandle
|
|
149
|
+
llm.py LLM, BoundLLM, the retry loop
|
|
150
|
+
anthropic/ the anthropic backend (needs the anthropic
|
|
151
|
+
SDK): __init__ has anthropic_model,
|
|
152
|
+
ANTHROPIC_PRICING, AnthropicModelName;
|
|
153
|
+
messages_provider.py has the adapter
|
|
154
|
+
AnthropicMessagesProvider
|
|
155
|
+
openai/ the openai backend (needs the openai SDK):
|
|
156
|
+
__init__ has openai_model, OPENAI_PRICING,
|
|
157
|
+
OpenAIModelName; responses_provider.py has
|
|
158
|
+
the adapter OpenAIResponsesProvider
|
|
159
|
+
tracing/ OTel span tracing (needs opentelemetry-api,
|
|
160
|
+
install langchaint[otel]): __init__ has
|
|
161
|
+
TracedLLM, TracedBoundLLM, TracedStreamHandle
|
|
162
|
+
|
|
163
|
+
## Verification
|
|
164
|
+
|
|
165
|
+
Run `scripts/CI.sh`; it runs `pyrefly check`, `ruff check`, and `pytest` through `uv run`, so the tools resolve from the locked dev dependency group, not a hand-activated `.venv`.
|
|
166
|
+
All must pass with zero errors.
|
|
167
|
+
The tests are offline: they feed constructed SDK objects into the adapter helpers and drive `BoundLLM`/`StreamHandle` with stub providers, so they need no API keys.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# langchaint
|
|
2
|
+
|
|
3
|
+
Provider-neutral async LLM client over the official provider SDKs (anthropic >= 0.116.0, openai >= 2.45.0).
|
|
4
|
+
Alpha: the API is unstable and may change without notice.
|
|
5
|
+
|
|
6
|
+
## Design
|
|
7
|
+
|
|
8
|
+
**Generation only via binding.**
|
|
9
|
+
`LLM` has no generate methods.
|
|
10
|
+
`LLM.bind(...) -> BoundLLM[OutputT]` freezes everything that determines the cacheable prompt prefix: `system_prompt`, `tool_manager`, `inference_params`, `tool_choice`, `parallel_tool_calls`, `automatic_prompt_caching`.
|
|
11
|
+
`response_format` is frozen too, as the field that fixes the output type.
|
|
12
|
+
`automatic_prompt_caching` has no default: caching changes billing, so every `bind` states it.
|
|
13
|
+
A `TextPart` or `ImagePart` with `cache_breakpoint=True` places a prompt-cache boundary at exactly that part, under either `automatic_prompt_caching` value, so binding `False` and marking parts is the fully user-specified caching configuration.
|
|
14
|
+
`generate_many(conversations, warm_cache=True)` runs the first conversation to completion before admitting the rest, so a batch sharing a cached prefix pays one cache write instead of one per in-flight item; it is opt-in because it costs one item of serial latency.
|
|
15
|
+
`system_prompt` also binds as a sequence of `TextPart`s, so a boundary can sit inside the frozen prefix (stable instructions marked, semi-stable context after).
|
|
16
|
+
There are no per-call parameter overrides; changing parameters is `rebind(...)`, which returns a new `BoundLLM` with the SDK keyword arguments converted again.
|
|
17
|
+
`bind(response_format=Model)` returns `BoundLLM[Model]`; without `response_format` it returns `BoundLLM[str]`, selected by overload.
|
|
18
|
+
`rebind` carries the same overload, so `rebind(response_format=Model)` switches the output type to `BoundLLM[Model]`, `rebind(response_format=None)` switches it back to `BoundLLM[str]`, and leaving it out keeps the current type.
|
|
19
|
+
Every generate and stream method takes a conversation of `Message`s; a bare `str` is accepted as shorthand for a conversation of one `UserMessage` holding that text.
|
|
20
|
+
|
|
21
|
+
**The catalog is a constructor function per backend returning a ready LLM (anthropic adds a second for Bedrock).**
|
|
22
|
+
`from langchaint.openai import openai_model` then `openai_model("gpt-5.6-terra")`, and `from langchaint.anthropic import anthropic_model` then `anthropic_model("claude-sonnet-5")`, take the provider's own model identifier (a `Literal`, so typos fail the type check), look up the public prices, construct the adapter, and wrap it in an `LLM`.
|
|
23
|
+
`client=None` constructs the native first-party SDK client from environment credentials.
|
|
24
|
+
Anthropic Bedrock is a second sibling constructor, `anthropic_bedrock_model(model, aws_region=...)`: it names the same catalog model and reads the model's Bedrock surface (which of two SDK client classes) and wire model id from a table, so the application names neither the client class nor the Bedrock id.
|
|
25
|
+
Constructing an adapter directly covers models outside the catalog.
|
|
26
|
+
|
|
27
|
+
**The SDKs are optional dependencies, one extra per backend.**
|
|
28
|
+
The neutral core (`LLM`, the message tree, the error taxonomy) imports no SDK, so `import langchaint` needs neither package.
|
|
29
|
+
Each backend lives in its own subpackage that imports its SDK at module top: install `langchaint[openai]` or `langchaint[anthropic]` (or `langchaint[all]`) for the ones you use, and importing `langchaint.openai` without the openai package raises a `ModuleNotFoundError` naming the extra to install.
|
|
30
|
+
The import path is the boundary: only code that reaches for a backend requires its SDK, and a type checker following `langchaint.openai` never resolves anthropic's types.
|
|
31
|
+
OTel tracing follows the same pattern: `langchaint.tracing` imports only opentelemetry-api, install `langchaint[otel]`, and it stays off `import langchaint`.
|
|
32
|
+
|
|
33
|
+
**Adapters wrap SDK clients and delegate to the SDK.**
|
|
34
|
+
`AnthropicMessagesProvider(client=AsyncAnthropic(...))` and `OpenAIResponsesProvider(client=AsyncOpenAI(...))` call `messages.create/parse/stream` and `responses.create/parse/stream`; stream assembly and structured-output parsing are the SDK's, not hand-written.
|
|
35
|
+
Adapters store a `with_options(max_retries=0)` copy of the client, so the SDK never retries beneath the package's retry loop.
|
|
36
|
+
OpenAI support is the Responses API only: every supported OpenAI model speaks it, so there is no Chat Completions adapter.
|
|
37
|
+
The adapter always sends `store=False` because conversation state is the caller's conversation argument.
|
|
38
|
+
Anthropic Bedrock is two distinct SDK surfaces, the legacy `InvokeModel` client `AsyncAnthropicBedrock` and the Messages-API client `AsyncAnthropicBedrockMantle`, and the catalog models split across them, so `anthropic_bedrock_model` reads the client class and wire model id per model from `ANTHROPIC_BEDROCK`.
|
|
39
|
+
OpenAI on Bedrock is the bundled `AsyncBedrockOpenAI` passed as `client`; there is no Converse adapter.
|
|
40
|
+
Requests travel as typed frozen dataclasses whose optional fields are `X | Omit`, passed to the SDK as explicit keywords: no `**kwargs`, no hand-written wire TypedDicts, and the SDK overloads resolve without casts.
|
|
41
|
+
|
|
42
|
+
**Anthropic prompt caching is placed by the adapter.**
|
|
43
|
+
A bind-time `cache_control` marker goes on the system block (or the last tool when there is no system prompt), and a per-request marker goes on the last block of the last message, so the breakpoint follows a growing conversation.
|
|
44
|
+
Placement is manual because `messages.parse` lacks the top-level `cache_control` parameter; manual placement keeps create/parse/stream uniform.
|
|
45
|
+
Every marker's TTL is the adapter's `cache_ttl` (`"5m"` default, `"1h"` for entries that must survive longer gaps at 2x write cost), a keyword of `anthropic_model` and `anthropic_bedrock_model`.
|
|
46
|
+
|
|
47
|
+
**Usage counters partition, cost is the adapter's job.**
|
|
48
|
+
`Usage` stores `input_tokens_cache_read`, `input_tokens_cache_write`, `input_tokens_cache_none`, and `output_tokens`; the three input counters are a disjoint partition and `input_tokens_total` is derived.
|
|
49
|
+
`input_tokens_total_provider_reported` holds what the provider itself reported: openai's `input_tokens` includes cached and cache-write tokens, so the adapter fills it and a validator cross-checks the partition; anthropic reports no all-inclusive total, so it stays None.
|
|
50
|
+
The counters are validated non-negative, which is what guards the openai path: there the partition is derived by subtraction and sums to the reported total by construction.
|
|
51
|
+
`cost_in_usd` is computed inside the adapter from raw provider counts against a `PricingTable`, because providers split counters the normalized `Usage` collapses: anthropic bills 5-minute and 1-hour cache writes at different rates, and an adapter that sees 1-hour writes without `cache_write_1h_usd_per_million_tokens` raises `AbortBatchError` rather than misbill.
|
|
52
|
+
|
|
53
|
+
**Success is a `Response`, failure is a `GenerationError`.**
|
|
54
|
+
A generate that succeeds returns a frozen `Response[OutputT]` with every field present; one that ends terminally raises (or, in a batch, returns) a `GenerationError`.
|
|
55
|
+
Its three leaves are the terminal per-item outcomes: `RetriesExhaustedError` (transient budget spent), `RefusalError` (the model refused on the structured path), and `ExceededMaxCompletionTokensError` (the structured response hit the token cap before its JSON parsed).
|
|
56
|
+
On the base, `stop_reason` is `StopReason | None`: `"refusal"` or `"max_tokens"` on those two leaves, `None` on `RetriesExhaustedError`, whose attempts never reached a completed turn.
|
|
57
|
+
Both hold `attempt_records`, one `AttemptRecord` per request sent: raw `time.monotonic()` start and end readings bracketing the send only (slot waits and backoff sleeps excluded, so rate limiting is distinguishable from slow requests), the attempt's `TransientError` (None on the attempt that succeeded or a rejected 200), and the attempt's `usage`/`cost_in_usd` (None for a transport failure that billed nothing).
|
|
58
|
+
On a `Response` every record but the last failed; on a `GenerationError` the records describe the terminal outcome.
|
|
59
|
+
`attempts` is derived from the records on both.
|
|
60
|
+
Both also carry `model`, `provider_name`, and `elapsed_seconds` (first request to completion, waits included, so stored rather than derived), so the module-level `to_row(result)` flattens either to the same scalar keys and a mixed list of successes and failures is one table.
|
|
61
|
+
A `GenerationError` derives `usage`/`cost_in_usd` from its records, so a refusal or truncation reports its real cost while a retry-exhausted item whose attempts billed nothing reports zero; `error_text` carries the failure reason.
|
|
62
|
+
On a `Response`, `usage`, `cost_in_usd` (the successful attempt's own), `stop_reason`, `assistant_message`, and `raw` are always present.
|
|
63
|
+
`raw` is the SDK's own response model held by reference; it is never dumped to a dict, because dumping deep-copies every response body per request.
|
|
64
|
+
|
|
65
|
+
**One RateLimiter owns retrying and pacing.**
|
|
66
|
+
`RateLimiter` holds `max_attempts`, `backoff_base_seconds`, `backoff_max_seconds`, and `max_in_flight`; it is stateful and shareable, so one instance passed to several `LLM`s is one shared budget for the account they hit.
|
|
67
|
+
Its slot gates every request start on every path (first attempts, retries, batch items, stream openings); backoff sleeps outside the slot.
|
|
68
|
+
There is deliberately no `requests_per_minute`: an in-flight bound self-adjusts throughput along request duration, while a client-side rate number models one dimension of the provider's multi-dimensional limit and goes stale with the account tier.
|
|
69
|
+
A rate-limit error (`Provider.classify` returning `"rate_limit"`, or any error naming a server-stated retry-after) pauses admission for everyone sharing the limiter: for the server-stated wait when one was sent (parsed from the `retry-after-ms` / `retry-after` headers onto `TransientError.retry_after_seconds`, capped at 60 seconds), else for the failing task's backoff delay.
|
|
70
|
+
After the pause, admission stays limited to one probe request at a time until the probe succeeds; other transient errors (timeouts, 5xx) pause nobody, because they say nothing about the account's quota.
|
|
71
|
+
|
|
72
|
+
**Retry stays in the package, classification in the adapter.**
|
|
73
|
+
Adapters raise `TransientError`, `AbortBatchError`, or a `GenerationError` leaf directly where they know the answer: rate limits and overload (transient), a bad request (abort), and an empty structured parse, which splits three ways (a refusal is `RefusalError`, a token-cap truncation is `ExceededMaxCompletionTokensError`, anything else is `TransientError`).
|
|
74
|
+
The retry loop retries only `TransientError`; it honors the rest without asking.
|
|
75
|
+
Unrecognized exceptions go through `Provider.classify`, which returns `"rate_limit"`, `"transient"`, or `"abort"`, and anything it does not recognize is abort.
|
|
76
|
+
`generate_one` raises `RetriesExhaustedError` for transient exhaustion, `RefusalError` or `ExceededMaxCompletionTokensError` on the structured path, and `AbortBatchError` for an abort classification; the first three share the `GenerationError` base a caller can catch at once.
|
|
77
|
+
`generate_many` returns `list[Response[OutputT] | GenerationError]`, so a terminal per-item failure is a row in its slot rather than a raise, while an `AbortBatchError` still cancels the siblings and raises; results stay order-aligned.
|
|
78
|
+
|
|
79
|
+
**Streaming is a handle.**
|
|
80
|
+
`stream_one` returns a `StreamHandle`: an async iterator of `StreamItem = str | ToolCall`, an idempotent `await handle.final()` returning the assembled `Response`, and an async context manager so abandoning a stream closes the connection.
|
|
81
|
+
Text chunks are the provider SDK's own strings passed through without a wrapper class or copy, and each `ToolCall` is yielded once, complete, when its block closes.
|
|
82
|
+
There are deliberately no tool-call delta items (a consumer cannot act on partial argument JSON, and both SDKs accumulate the arguments and hand over the finished call) and no usage or stop items (usage, `cost_in_usd`, and `stop_reason` live on `final()`'s `Response`, and a bare-`str` stop reason could not share a union with bare-`str` text chunks).
|
|
83
|
+
Connection failures before the first yielded item retry under the `RateLimiter`; after the first yielded item nothing retries, because replaying chunks the caller already consumed would duplicate output.
|
|
84
|
+
An open stream holds one `RateLimiter` slot from opening until it closes or exhausts, so long-lived streams count against `max_in_flight`.
|
|
85
|
+
|
|
86
|
+
**Tools are explicit, dispatch is owned.**
|
|
87
|
+
A `Tool` is an async function taking one pydantic model and returning str or a sequence of content parts (text and images the model then sees), plus an explicit name and description; the args model is the schema source, so there is no signature introspection and no docstring scraping.
|
|
88
|
+
Tool content is model-facing, so it is exactly `MessageContent` (`str | Sequence[Part]`, the model-facing message body, aliased in `messages.py`): a function with a typed result serializes it to that form itself.
|
|
89
|
+
A function may instead return a `ToolOutputExplicit` wrapping that content plus `is_error` and `app_data` (the app-facing channel, which does carry a typed `BaseModel` or `Mapping` the model never sees).
|
|
90
|
+
`Tool.validate_and_run` validates raw call JSON against `args_model` and runs the function; it lives on `Tool` because there the args type parameter is concrete, so the validated arguments reach the function fully typed.
|
|
91
|
+
`Tool.dispatch` returns `DispatchHandled[AppDataT] | DispatchInvalidToolArgs`, so on the handled outcome a caller that dispatched a known tool reads `app_data` back at its concrete type with no `isinstance`.
|
|
92
|
+
`ToolManager` serializes the tools provider-neutrally, resolves the called name, and returns each outcome as a `DispatchOutcome`, a three-arm union over a heterogeneous tool set (where `app_data` erases to `BaseModel | Mapping[str, object] | None`): `DispatchHandled` carries the model-facing `ToolMessage` the caller appends to the conversation, paired with the function's `app_data` (data the model never sees); `DispatchInvalidToolArgs` carries a default `is_error` `ToolMessage` plus the pydantic `ValidationError` as a required field, so a caller that authors its own reply reads `validation_error.errors()` with no narrowing crutch; `DispatchUnknownTool` carries a default `is_error` `ToolMessage` naming the held tools plus the off-list `called_name`.
|
|
93
|
+
Every arm carries `tool_message`, so a caller that only appends the reply reads `result.tool_message` with no `match`.
|
|
94
|
+
An off-list tool name becomes a `DispatchUnknownTool` outcome rather than a raise, just like argument JSON the model got wrong becomes a `DispatchInvalidToolArgs` outcome: both are model data the model can correct (a provider can emit a name outside the sent schemas, and a rebind can strand an earlier turn's `tool_call`).
|
|
95
|
+
`tool_choice` is the neutral vocabulary `"auto" | "required" | SpecificTool | "none"` (anthropic's `"any"` maps from `"required"`); `parallel_tool_calls: bool` maps to anthropic `disable_parallel_tool_use` and openai `parallel_tool_calls`.
|
|
96
|
+
|
|
97
|
+
**The ReAct loop is a recipe, not vendored code.**
|
|
98
|
+
There is no agent class.
|
|
99
|
+
The loop is ~15 lines and is correct because the hard parts (retries, pacing, classification, validation) live below `generate_one` and `dispatch`, so a hand-copied loop cannot be subtly wrong.
|
|
100
|
+
The non-streaming loop runs over `generate_one`; the streaming loop runs over `stream_one`, printing text chunks as they arrive and dispatching the completed `ToolCall`s between turns, reading `dispatch(call).tool_message` for the ToolMessage it appends and matching `DispatchHandled` for the function's model-invisible `app_data` or `DispatchInvalidToolArgs` for the `validation_error`.
|
|
101
|
+
Owning the loop is what lets a caller enforce a budget mid-run, stream tokens, or swap the binding on a tool result, none of which a fixed agent surface exposes.
|
|
102
|
+
|
|
103
|
+
## Layout
|
|
104
|
+
|
|
105
|
+
CLAUDE.md design, naming, and review rules
|
|
106
|
+
README.md this file
|
|
107
|
+
src/langchaint/
|
|
108
|
+
messages.py message and part models, StopReason
|
|
109
|
+
usage.py Usage counters
|
|
110
|
+
inference_params.py InferenceParams
|
|
111
|
+
exceptions.py error vocabulary: TransientError,
|
|
112
|
+
AbortBatchError, AttemptRecord,
|
|
113
|
+
GenerationError and its leaves
|
|
114
|
+
response.py Response[OutputT], to_row
|
|
115
|
+
rate_limiter.py RateLimiter
|
|
116
|
+
tools.py Tool, ToolSchema, ToolManager
|
|
117
|
+
provider.py Binding, ToolChoice, PricingTable,
|
|
118
|
+
StreamItem, ProviderResult,
|
|
119
|
+
ProviderStream, BoundProvider, Provider
|
|
120
|
+
streaming.py StreamHandle
|
|
121
|
+
llm.py LLM, BoundLLM, the retry loop
|
|
122
|
+
anthropic/ the anthropic backend (needs the anthropic
|
|
123
|
+
SDK): __init__ has anthropic_model,
|
|
124
|
+
ANTHROPIC_PRICING, AnthropicModelName;
|
|
125
|
+
messages_provider.py has the adapter
|
|
126
|
+
AnthropicMessagesProvider
|
|
127
|
+
openai/ the openai backend (needs the openai SDK):
|
|
128
|
+
__init__ has openai_model, OPENAI_PRICING,
|
|
129
|
+
OpenAIModelName; responses_provider.py has
|
|
130
|
+
the adapter OpenAIResponsesProvider
|
|
131
|
+
tracing/ OTel span tracing (needs opentelemetry-api,
|
|
132
|
+
install langchaint[otel]): __init__ has
|
|
133
|
+
TracedLLM, TracedBoundLLM, TracedStreamHandle
|
|
134
|
+
|
|
135
|
+
## Verification
|
|
136
|
+
|
|
137
|
+
Run `scripts/CI.sh`; it runs `pyrefly check`, `ruff check`, and `pytest` through `uv run`, so the tools resolve from the locked dev dependency group, not a hand-activated `.venv`.
|
|
138
|
+
All must pass with zero errors.
|
|
139
|
+
The tests are offline: they feed constructed SDK objects into the adapter helpers and drive `BoundLLM`/`StreamHandle` with stub providers, so they need no API keys.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "langchaint"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "It ain't langchain."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "John Hopfensperger" }]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
keywords = ["llm", "anthropic", "openai", "claude", "async"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Typing :: Typed",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"pydantic>=2.13.4",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/s-banach/langchaint"
|
|
23
|
+
Repository = "https://github.com/s-banach/langchaint"
|
|
24
|
+
Issues = "https://github.com/s-banach/langchaint/issues"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
# One extra per backend: the langchaint.anthropic / langchaint.openai subpackages
|
|
28
|
+
# import their SDK at module top, so an application installs only the backends it uses.
|
|
29
|
+
anthropic = ["anthropic>=0.116.0"]
|
|
30
|
+
openai = ["openai>=2.45.0"]
|
|
31
|
+
all = ["anthropic>=0.116.0", "openai>=2.45.0"]
|
|
32
|
+
# The langchaint.tracing subpackage imports only opentelemetry-api (trace, Tracer,
|
|
33
|
+
# Status, StatusCode); a production app installs the api and wires its own SDK,
|
|
34
|
+
# matching OTel's api/sdk split. Deliberately not folded into all, which stays
|
|
35
|
+
# backends-only: an app that wants both SDKs but no tracing must not pull OTel.
|
|
36
|
+
otel = ["opentelemetry-api>=1.30.0"]
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["uv_build>=0.11.26,<0.12.0"]
|
|
40
|
+
build-backend = "uv_build"
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"langchaint[all]",
|
|
45
|
+
"langchaint[otel]",
|
|
46
|
+
# opentelemetry-sdk is needed only by the tracing tests' in-memory exporter
|
|
47
|
+
# (InMemorySpanExporter, SimpleSpanProcessor, TracerProvider); the wrapper
|
|
48
|
+
# itself imports only opentelemetry-api.
|
|
49
|
+
"opentelemetry-sdk>=1.30.0",
|
|
50
|
+
"pytest>=9.0.2",
|
|
51
|
+
"pyrefly>=1.2.0.dev2",
|
|
52
|
+
"ruff>=0.15.20",
|
|
53
|
+
"typos>=1.48.0",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.pyrefly]
|
|
57
|
+
preset = "strict"
|
|
58
|
+
# project-includes bounds type checking to exactly these trees (leaving the developer
|
|
59
|
+
# tooling under scripts/ out); search-path is the separate import-resolution setting, "src"
|
|
60
|
+
# being the langchaint import root.
|
|
61
|
+
project-includes = ["src", "tests", "examples"]
|
|
62
|
+
# src is the langchaint import root; the repo root is added so cross-module test
|
|
63
|
+
# imports (tests.test_bound_llm reused by tests.test_tracing) resolve the same way
|
|
64
|
+
# pytest resolves them at runtime.
|
|
65
|
+
search-path = ["src", "."]
|
|
66
|
+
|
|
67
|
+
# Enabled above strict: redundant-cast enforces the "no cast" smell's
|
|
68
|
+
# stale half (every finding is a deletable cast), and deprecated flags
|
|
69
|
+
# stdlib/library deprecations before an upgrade breaks them.
|
|
70
|
+
# explicit-any stays off: the remaining Any sites are deliberate
|
|
71
|
+
# existential typing (overload implementations, SDK stream generics,
|
|
72
|
+
# the heterogeneous Tool collection), and suppressions on deliberate
|
|
73
|
+
# Any would only add noise. When pyrefly 1.2.0 lands, also enable
|
|
74
|
+
# implicit-any-variable (inference gaps that silently make a variable
|
|
75
|
+
# Any): the tree is clean for it since Tool.validate_and_run owns the
|
|
76
|
+
# typed validate-then-call pair, but 1.1.1 rejects the rule name.
|
|
77
|
+
[tool.pyrefly.errors]
|
|
78
|
+
redundant-cast = "error"
|
|
79
|
+
deprecated = "error"
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
testpaths = ["tests"]
|
|
83
|
+
|
|
84
|
+
[tool.ruff]
|
|
85
|
+
line-length = 99
|
|
86
|
+
format.preview = true
|
|
87
|
+
|
|
88
|
+
lint.preview = true
|
|
89
|
+
lint.explicit-preview-rules = true
|
|
90
|
+
lint.select = [
|
|
91
|
+
"ANN",
|
|
92
|
+
"BLE",
|
|
93
|
+
"FBT",
|
|
94
|
+
"PERF",
|
|
95
|
+
"A",
|
|
96
|
+
"B",
|
|
97
|
+
"INP",
|
|
98
|
+
"PIE",
|
|
99
|
+
"PT",
|
|
100
|
+
"RET",
|
|
101
|
+
"RSE",
|
|
102
|
+
"SLF",
|
|
103
|
+
"SIM",
|
|
104
|
+
"SLOT",
|
|
105
|
+
"TID",
|
|
106
|
+
"TC",
|
|
107
|
+
"ARG",
|
|
108
|
+
"LOG",
|
|
109
|
+
"G",
|
|
110
|
+
"PTH",
|
|
111
|
+
"FLY",
|
|
112
|
+
"I",
|
|
113
|
+
"N",
|
|
114
|
+
"F",
|
|
115
|
+
"PL",
|
|
116
|
+
"UP",
|
|
117
|
+
"FURB",
|
|
118
|
+
"RUF",
|
|
119
|
+
"TRY",
|
|
120
|
+
"W505", # doc-line-too-long, capped by pycodestyle.max-doc-length below
|
|
121
|
+
# Docstrings, selected as exact codes with no pydocstyle convention.
|
|
122
|
+
# The dropped google convention was an ignore-overlay whose removals
|
|
123
|
+
# the code satisfies anyway, and it ignored D401, which then needed an
|
|
124
|
+
# exact-code select to win over the convention. Deliberately absent:
|
|
125
|
+
# D203 and D213, which conflict with D211 and D212; D405-D414 and
|
|
126
|
+
# D416, which enforce underlined rst/numpy section headers instead of
|
|
127
|
+
# the Args:/Raises:/Yields: prose sections used here; and the preview
|
|
128
|
+
# rules D420 (incorrect-section-order) and D421
|
|
129
|
+
# (property-docstring-starts-with-verb), not opted into: D421 flags a
|
|
130
|
+
# verb-first property docstring, which the imperative mood used
|
|
131
|
+
# everywhere here (D401) would otherwise produce.
|
|
132
|
+
"D100", # undocumented-public-module
|
|
133
|
+
"D101", # undocumented-public-class
|
|
134
|
+
"D102", # undocumented-public-method
|
|
135
|
+
"D103", # undocumented-public-function
|
|
136
|
+
"D104", # undocumented-public-package
|
|
137
|
+
"D105", # undocumented-magic-method
|
|
138
|
+
"D106", # undocumented-public-nested-class
|
|
139
|
+
"D107", # undocumented-public-init
|
|
140
|
+
"D200", # unnecessary-multiline-docstring
|
|
141
|
+
"D201", # blank-line-before-function
|
|
142
|
+
"D202", # blank-line-after-function
|
|
143
|
+
"D204", # incorrect-blank-line-after-class
|
|
144
|
+
"D205", # missing-blank-line-after-summary
|
|
145
|
+
"D206", # docstring-tab-indentation
|
|
146
|
+
"D207", # under-indentation
|
|
147
|
+
"D208", # over-indentation
|
|
148
|
+
"D209", # new-line-after-last-paragraph
|
|
149
|
+
"D210", # surrounding-whitespace
|
|
150
|
+
"D211", # blank-line-before-class
|
|
151
|
+
"D212", # multi-line-summary-first-line
|
|
152
|
+
"D214", # overindented-section
|
|
153
|
+
"D215", # overindented-section-underline
|
|
154
|
+
"D300", # triple-single-quotes
|
|
155
|
+
"D301", # escape-sequence-in-docstring
|
|
156
|
+
"D400", # missing-trailing-period
|
|
157
|
+
"D401", # non-imperative-mood
|
|
158
|
+
"D402", # signature-in-docstring
|
|
159
|
+
"D403", # first-word-uncapitalized
|
|
160
|
+
"D404", # docstring-starts-with-this
|
|
161
|
+
"D415", # missing-terminal-punctuation
|
|
162
|
+
"D417", # undocumented-param
|
|
163
|
+
"D418", # overload-with-docstring
|
|
164
|
+
"D419", # empty-docstring
|
|
165
|
+
# Preview docstring features
|
|
166
|
+
"DOC202", # docstring documents a return that does not exist
|
|
167
|
+
"DOC402", # missing "Yields" in docstring
|
|
168
|
+
"DOC403", # docstring documents a yield that does not exist
|
|
169
|
+
# DOC501 (raised exception missing) and DOC502 (documents an exception not
|
|
170
|
+
# raised) are deliberately absent. Both resolve a raise only for the literal
|
|
171
|
+
# `raise SomeError(...)` / `raise SomeError` shapes; they cannot see through
|
|
172
|
+
# `raise type(exc)(...)` (re-raising the same leaf class enriched) or
|
|
173
|
+
# `raise Class.factory(...)` (they read the last attribute, `factory`, as the
|
|
174
|
+
# exception name), both of which this package uses. Under them a correctly
|
|
175
|
+
# documented raise reports as extraneous and a real raise reports as missing,
|
|
176
|
+
# so documenting raises is a CLAUDE.md convention (see "Document what a
|
|
177
|
+
# function raises") checked in review instead.
|
|
178
|
+
]
|
|
179
|
+
lint.ignore = [
|
|
180
|
+
"ANN401",
|
|
181
|
+
"SIM105",
|
|
182
|
+
"PLR2004",
|
|
183
|
+
"TRY003",
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
# Docstrings and comments break lines at sentence boundaries, not at a column
|
|
187
|
+
# width; a sentence longer than this cap breaks at a clause boundary. Code
|
|
188
|
+
# stays at line-length 99 via the formatter; E501 is deliberately unselected.
|
|
189
|
+
lint.pycodestyle.max-doc-length = 120
|
|
190
|
+
lint.flake8-tidy-imports.ban-relative-imports = "all"
|
|
191
|
+
lint.pylint.max-args = 7
|
|
192
|
+
|
|
193
|
+
# The tests exercise private helpers and attributes directly (Provider._request,
|
|
194
|
+
# BoundLLM._bound_provider); those accesses are the unit under test, so SLF001
|
|
195
|
+
# (private-member-access) cannot be satisfied without exposing internals.
|
|
196
|
+
lint.per-file-ignores."tests/*" = ["SLF001"]
|
|
197
|
+
|
|
198
|
+
# The examples are standalone scripts run by path (python examples/01_basics.py),
|
|
199
|
+
# deliberately not an importable package, so INP001 (implicit-namespace-package,
|
|
200
|
+
# which would demand an examples/__init__.py) does not apply.
|
|
201
|
+
lint.per-file-ignores."examples/*" = ["INP001"]
|
|
202
|
+
|
|
203
|
+
[tool.typos.default.extend-words]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""langchaint: a provider-neutral LLM client.
|
|
2
|
+
|
|
3
|
+
Adapters wrap the official anthropic/openai SDK clients; generation happens only through LLM.bind(...) -> BoundLLM.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from langchaint.exceptions import (
|
|
7
|
+
AbortBatchError,
|
|
8
|
+
AttemptRecord,
|
|
9
|
+
ExceededMaxCompletionTokensError,
|
|
10
|
+
GenerationError,
|
|
11
|
+
InvalidToolArgsError,
|
|
12
|
+
RefusalError,
|
|
13
|
+
RetriesExhaustedError,
|
|
14
|
+
StreamProtocolError,
|
|
15
|
+
TransientError,
|
|
16
|
+
)
|
|
17
|
+
from langchaint.inference_params import InferenceParams, ReasoningEffort
|
|
18
|
+
from langchaint.llm import LLM, BoundLLM
|
|
19
|
+
from langchaint.messages import (
|
|
20
|
+
AssistantMessage,
|
|
21
|
+
ImagePart,
|
|
22
|
+
Message,
|
|
23
|
+
MessageContent,
|
|
24
|
+
Part,
|
|
25
|
+
ReasoningTrace,
|
|
26
|
+
StopReason,
|
|
27
|
+
TextPart,
|
|
28
|
+
ToolCall,
|
|
29
|
+
ToolMessage,
|
|
30
|
+
TurnElement,
|
|
31
|
+
UserMessage,
|
|
32
|
+
)
|
|
33
|
+
from langchaint.provider import (
|
|
34
|
+
PricingTable,
|
|
35
|
+
SpecificTool,
|
|
36
|
+
StreamItem,
|
|
37
|
+
ToolChoice,
|
|
38
|
+
)
|
|
39
|
+
from langchaint.rate_limiter import RateLimiter
|
|
40
|
+
from langchaint.response import Response, RowValue, to_row
|
|
41
|
+
from langchaint.streaming import StreamHandle
|
|
42
|
+
from langchaint.tools import (
|
|
43
|
+
DispatchHandled,
|
|
44
|
+
DispatchInvalidToolArgs,
|
|
45
|
+
DispatchOutcome,
|
|
46
|
+
DispatchUnknownTool,
|
|
47
|
+
Tool,
|
|
48
|
+
ToolManager,
|
|
49
|
+
ToolOutput,
|
|
50
|
+
ToolOutputExplicit,
|
|
51
|
+
)
|
|
52
|
+
from langchaint.usage import Usage
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"LLM",
|
|
56
|
+
"AbortBatchError",
|
|
57
|
+
"AssistantMessage",
|
|
58
|
+
"AttemptRecord",
|
|
59
|
+
"BoundLLM",
|
|
60
|
+
"DispatchHandled",
|
|
61
|
+
"DispatchInvalidToolArgs",
|
|
62
|
+
"DispatchOutcome",
|
|
63
|
+
"DispatchUnknownTool",
|
|
64
|
+
"ExceededMaxCompletionTokensError",
|
|
65
|
+
"GenerationError",
|
|
66
|
+
"ImagePart",
|
|
67
|
+
"InferenceParams",
|
|
68
|
+
"InvalidToolArgsError",
|
|
69
|
+
"Message",
|
|
70
|
+
"MessageContent",
|
|
71
|
+
"Part",
|
|
72
|
+
"PricingTable",
|
|
73
|
+
"RateLimiter",
|
|
74
|
+
"ReasoningEffort",
|
|
75
|
+
"ReasoningTrace",
|
|
76
|
+
"RefusalError",
|
|
77
|
+
"Response",
|
|
78
|
+
"RetriesExhaustedError",
|
|
79
|
+
"RowValue",
|
|
80
|
+
"SpecificTool",
|
|
81
|
+
"StopReason",
|
|
82
|
+
"StreamHandle",
|
|
83
|
+
"StreamItem",
|
|
84
|
+
"StreamProtocolError",
|
|
85
|
+
"TextPart",
|
|
86
|
+
"Tool",
|
|
87
|
+
"ToolCall",
|
|
88
|
+
"ToolChoice",
|
|
89
|
+
"ToolManager",
|
|
90
|
+
"ToolMessage",
|
|
91
|
+
"ToolOutput",
|
|
92
|
+
"ToolOutputExplicit",
|
|
93
|
+
"TransientError",
|
|
94
|
+
"TurnElement",
|
|
95
|
+
"Usage",
|
|
96
|
+
"UserMessage",
|
|
97
|
+
"to_row",
|
|
98
|
+
]
|