thinwrap-llm 1.1.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 (35) hide show
  1. thinwrap_llm-1.1.0/LICENSE +21 -0
  2. thinwrap_llm-1.1.0/PKG-INFO +181 -0
  3. thinwrap_llm-1.1.0/README.md +162 -0
  4. thinwrap_llm-1.1.0/pyproject.toml +39 -0
  5. thinwrap_llm-1.1.0/setup.cfg +4 -0
  6. thinwrap_llm-1.1.0/src/thinwrap/llm/__init__.py +51 -0
  7. thinwrap_llm-1.1.0/src/thinwrap/llm/_anthropic.py +526 -0
  8. thinwrap_llm-1.1.0/src/thinwrap/llm/_awssigv4.py +95 -0
  9. thinwrap_llm-1.1.0/src/thinwrap/llm/_bedrock.py +454 -0
  10. thinwrap_llm-1.1.0/src/thinwrap/llm/_gemini.py +590 -0
  11. thinwrap_llm-1.1.0/src/thinwrap/llm/_jsonpath.py +43 -0
  12. thinwrap_llm-1.1.0/src/thinwrap/llm/_openai_compat.py +345 -0
  13. thinwrap_llm-1.1.0/src/thinwrap/llm/_openai_compat_embeddings.py +124 -0
  14. thinwrap_llm-1.1.0/src/thinwrap/llm/_util.py +30 -0
  15. thinwrap_llm-1.1.0/src/thinwrap/llm/base.py +265 -0
  16. thinwrap_llm-1.1.0/src/thinwrap/llm/chat.py +200 -0
  17. thinwrap_llm-1.1.0/src/thinwrap/llm/config.py +54 -0
  18. thinwrap_llm-1.1.0/src/thinwrap/llm/embeddings.py +57 -0
  19. thinwrap_llm-1.1.0/src/thinwrap/llm/enums.py +28 -0
  20. thinwrap_llm-1.1.0/src/thinwrap/llm/errors.py +188 -0
  21. thinwrap_llm-1.1.0/src/thinwrap/llm/passthrough.py +30 -0
  22. thinwrap_llm-1.1.0/src/thinwrap/llm/py.typed +0 -0
  23. thinwrap_llm-1.1.0/src/thinwrap/llm/spec.py +66 -0
  24. thinwrap_llm-1.1.0/src/thinwrap/llm/sse.py +53 -0
  25. thinwrap_llm-1.1.0/src/thinwrap/llm/transport.py +280 -0
  26. thinwrap_llm-1.1.0/src/thinwrap_llm.egg-info/PKG-INFO +181 -0
  27. thinwrap_llm-1.1.0/src/thinwrap_llm.egg-info/SOURCES.txt +33 -0
  28. thinwrap_llm-1.1.0/src/thinwrap_llm.egg-info/dependency_links.txt +1 -0
  29. thinwrap_llm-1.1.0/src/thinwrap_llm.egg-info/requires.txt +4 -0
  30. thinwrap_llm-1.1.0/src/thinwrap_llm.egg-info/top_level.txt +1 -0
  31. thinwrap_llm-1.1.0/tests/test_anthropic.py +734 -0
  32. thinwrap_llm-1.1.0/tests/test_bedrock.py +501 -0
  33. thinwrap_llm-1.1.0/tests/test_core.py +951 -0
  34. thinwrap_llm-1.1.0/tests/test_dispatch_all.py +70 -0
  35. thinwrap_llm-1.1.0/tests/test_gemini.py +593 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitry Polyanovsky & contributors
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,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: thinwrap-llm
3
+ Version: 1.1.0
4
+ Summary: Lightweight, SDK-free, zero-dependency Python wrapper for chat completions (with streaming) and embeddings across 18 LLM providers behind one normalized surface.
5
+ Author: Dmitry Polyanovsky
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://thinwrap.dev
8
+ Project-URL: Repository, https://github.com/thinwrap/llm-py
9
+ Keywords: llm,ai,chat,embeddings,openai,anthropic,bedrock,gemini,streaming
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Typing :: Typed
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == "dev"
17
+ Requires-Dist: pytest-cov; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # thinwrap-llm (Python)
21
+
22
+ Unified, **SDK-free**, **zero-runtime-dependency** Python wrapper for **chat
23
+ completions** (with streaming) and **embeddings** across 18 LLM providers. Switch
24
+ vendor by changing the provider id + model; the input and output shapes stay
25
+ identical. Synchronous, stateless, standard-library-only — bring your own HTTP
26
+ transport if you want `httpx`/`requests`.
27
+
28
+ ```bash
29
+ pip install thinwrap-llm
30
+ ```
31
+
32
+ Requires Python ≥ 3.10. No runtime dependencies — Bedrock's SigV4 is `hmac`-based
33
+ and SSE is parsed on the stdlib.
34
+
35
+ ## Two-minute chat
36
+
37
+ ```python
38
+ from thinwrap.llm import Chat, ChatInput, ChatMessage, OpenAICompatConfig, ProviderID, ConnectorError
39
+
40
+ chat = Chat(ProviderID.OPENAI, OpenAICompatConfig(api_key="..."))
41
+ try:
42
+ res = chat.complete(ChatInput(
43
+ model="gpt-4o-mini",
44
+ messages=[ChatMessage(role="user", content="Say hi in one word.")],
45
+ ))
46
+ print(res.message.content, res.usage.total_tokens)
47
+ except ConnectorError as e:
48
+ print(e.provider_code.value, e.provider_message)
49
+ ```
50
+
51
+ ## Streaming
52
+
53
+ ```python
54
+ for delta in chat.stream(ChatInput(model="gpt-4o-mini", messages=msgs)):
55
+ print(delta.content_delta, end="", flush=True)
56
+ ```
57
+
58
+ A stream that is cut off before the provider's terminal event, or a 2xx answer
59
+ that is not `text/event-stream`, raises `ConnectorError` (`provider_unavailable`)
60
+ rather than ending as if it were complete.
61
+
62
+ ## Usage
63
+
64
+ `res.usage` counts every billed token: `input_tokens` includes cache reads and
65
+ writes (Anthropic, Bedrock), `output_tokens` includes thinking tokens (Gemini), and
66
+ `total_tokens` is the vendor's total, or input + output when it reports none. When
67
+ streaming, `usage` arrives once, on the terminal delta.
68
+
69
+ ## Tool loops and `provider_metadata`
70
+
71
+ Some providers need opaque state from a response sent back on the next turn:
72
+ Gemini 3 rejects a replayed function call without its thought signature, and
73
+ Anthropic keeps its thinking blocks only if they are replayed verbatim.
74
+ `ToolCall.provider_metadata` and `AssistantMessage.provider_metadata` carry that
75
+ state, keyed by provider id (`{"gemini": {...}}`, `{"anthropic": {...}}`). Treat it
76
+ as opaque and copy it into the next request together with `content` and
77
+ `tool_calls`:
78
+
79
+ ```python
80
+ res = chat.complete(ChatInput(model=model, messages=msgs, tools=tools))
81
+ msgs.append(ChatMessage(role="assistant", content=res.message.content,
82
+ tool_calls=res.message.tool_calls,
83
+ provider_metadata=res.message.provider_metadata))
84
+ ```
85
+
86
+ When streaming, the state rides on `tool_call_delta.provider_metadata` (copy it
87
+ onto the `ToolCall` you assemble) and on the terminal delta's `provider_metadata`.
88
+ A connector reads only its own key, so a history survives a provider switch. If the
89
+ state is lost, Gemini falls back to Google's documented signature bypass, and
90
+ Anthropic rebuilds that turn and every later one without their thinking (Anthropic
91
+ invalidates thinking after a stripped turn), so the earlier reasoning is lost.
92
+
93
+ ## Providers
94
+
95
+ The **provider id selects the connector**; `ChatInput` / `ChatResult` are
96
+ identical across all of them.
97
+
98
+ **Chat (18):** the 15 first-class OpenAI-compatible providers —
99
+ `ProviderID.OPENAI`, `AZURE_OPENAI`, `OPENROUTER`, `GROQ`, `TOGETHER`,
100
+ `FIREWORKS`, `DEEPSEEK`, `XAI`, `MISTRAL`, `PERPLEXITY`, `DEEPINFRA`,
101
+ `CLOUDFLARE`, `VLLM`, `OLLAMA`, `LMSTUDIO` — all take `OpenAICompatConfig`; plus 3
102
+ natives: `ANTHROPIC` (`AnthropicConfig`), `BEDROCK` (`BedrockConfig`), `GEMINI`
103
+ (`GeminiConfig`).
104
+
105
+ **Embeddings (11):** the OpenAI-float subset — `OPENAI`, `AZURE_OPENAI`,
106
+ `OPENROUTER`, `TOGETHER`, `FIREWORKS`, `MISTRAL`, `DEEPINFRA`, `CLOUDFLARE`,
107
+ `VLLM`, `OLLAMA`, `LMSTUDIO`.
108
+
109
+ ```python
110
+ from thinwrap.llm import Embeddings, EmbeddingsInput, OpenAICompatConfig, ProviderID
111
+
112
+ emb = Embeddings(ProviderID.OPENAI, OpenAICompatConfig(api_key="..."))
113
+ out = emb.create(EmbeddingsInput(model="text-embedding-3-small", input=["hello", "world"]))
114
+ # out.embeddings is list[list[float]] in input order
115
+ ```
116
+
117
+ Per-provider details live in [`docs/providers/`](docs/providers/).
118
+
119
+ ## The ≥90% baseline & passthrough
120
+
121
+ Only fields ≥90% of providers support normalizably are first-class on
122
+ `ChatInput` / `ChatResult`; everything else rides through `Passthrough` (request)
123
+ / `raw` (response) and is **never emulated**:
124
+
125
+ ```python
126
+ from thinwrap.llm import Passthrough
127
+
128
+ res = chat.complete(ChatInput(model="...", messages=msgs,
129
+ passthrough=Passthrough(body={"logprobs": True})))
130
+ _ = res.raw # verbatim vendor body — reasoning CoT, cached-token counts, cost, ...
131
+ ```
132
+
133
+ ## Errors
134
+
135
+ Every failure raises `ConnectorError`: `provider_code` is one of `rate_limited`,
136
+ `auth_failed`, `provider_unavailable`, `invalid_request`,
137
+ `context_length_exceeded`, `content_filtered`, `unknown`. The raw `Retry-After`
138
+ rides in `cause["retryAfter"]` (+ parsed `cause["retryAfterSeconds"]`) — there is
139
+ no top-level retry attribute. `retryAfterSeconds` is set only for an integer delay
140
+ or an HTTP-date (`0` when the date has passed).
141
+
142
+ By HTTP status: `401`/`403` → `auth_failed`; `402` (out of credit) / `429` →
143
+ `rate_limited`; `408` / `5xx` → `provider_unavailable`; every other `4xx` →
144
+ `invalid_request`, unless the vendor's context-overflow or content-filter signal
145
+ matches. An error delivered inside a 2xx body or mid-stream keeps the HTTP status it
146
+ arrived with and is classified by its own code or type (`unknown` without one).
147
+
148
+ ## Bring your own transport
149
+
150
+ ```python
151
+ chat = Chat(ProviderID.OPENAI, OpenAICompatConfig(api_key="..."), transport=my_transport)
152
+ ```
153
+
154
+ `my_transport` implements `Transport` (`send(HttpRequest) -> HttpResponse`); add
155
+ an `open_stream` method for true incremental streaming (otherwise a buffered
156
+ fallback is used). The default stdlib `urllib` transport does both, never
157
+ follows redirects, and is HTTPS-only: the `http://` defaults of the local servers
158
+ (vLLM, Ollama, LM Studio) get an `invalid_request` telling you to bring your own
159
+ transport. Its socket timeout is 600 s per blocking read, so a long non-streaming
160
+ reasoning call is not cut off; pass `UrllibTransport(timeout=...)` for a different
161
+ budget. The wrapper is stateless — no caching, retries, or conversation memory.
162
+
163
+ ## Relationship to the siblings
164
+
165
+ One of four thinwrap llm libraries sharing a normalized surface and
166
+ byte-identical provider ids / error codes: TypeScript (`@thinwrap/llm`), PHP
167
+ (`thinwrap/llm`), Go (`thinwrap/llm-go`), Python (this package).
168
+
169
+ ## Security
170
+
171
+ Report vulnerabilities **privately** — please do not open a public issue. Preferred: a
172
+ [private security advisory](https://github.com/thinwrap/llm-py/security/advisories/new)
173
+ on this repository. Alternatively, email **security@thinwrap.dev**. Include the affected
174
+ versions and a minimal reproduction if you have one.
175
+
176
+ A vulnerability in a *provider's* own API or service belongs to that vendor rather than
177
+ to this wrapper — please report those upstream.
178
+
179
+ ## License
180
+
181
+ MIT © Dmitry Polyanovsky & contributors
@@ -0,0 +1,162 @@
1
+ # thinwrap-llm (Python)
2
+
3
+ Unified, **SDK-free**, **zero-runtime-dependency** Python wrapper for **chat
4
+ completions** (with streaming) and **embeddings** across 18 LLM providers. Switch
5
+ vendor by changing the provider id + model; the input and output shapes stay
6
+ identical. Synchronous, stateless, standard-library-only — bring your own HTTP
7
+ transport if you want `httpx`/`requests`.
8
+
9
+ ```bash
10
+ pip install thinwrap-llm
11
+ ```
12
+
13
+ Requires Python ≥ 3.10. No runtime dependencies — Bedrock's SigV4 is `hmac`-based
14
+ and SSE is parsed on the stdlib.
15
+
16
+ ## Two-minute chat
17
+
18
+ ```python
19
+ from thinwrap.llm import Chat, ChatInput, ChatMessage, OpenAICompatConfig, ProviderID, ConnectorError
20
+
21
+ chat = Chat(ProviderID.OPENAI, OpenAICompatConfig(api_key="..."))
22
+ try:
23
+ res = chat.complete(ChatInput(
24
+ model="gpt-4o-mini",
25
+ messages=[ChatMessage(role="user", content="Say hi in one word.")],
26
+ ))
27
+ print(res.message.content, res.usage.total_tokens)
28
+ except ConnectorError as e:
29
+ print(e.provider_code.value, e.provider_message)
30
+ ```
31
+
32
+ ## Streaming
33
+
34
+ ```python
35
+ for delta in chat.stream(ChatInput(model="gpt-4o-mini", messages=msgs)):
36
+ print(delta.content_delta, end="", flush=True)
37
+ ```
38
+
39
+ A stream that is cut off before the provider's terminal event, or a 2xx answer
40
+ that is not `text/event-stream`, raises `ConnectorError` (`provider_unavailable`)
41
+ rather than ending as if it were complete.
42
+
43
+ ## Usage
44
+
45
+ `res.usage` counts every billed token: `input_tokens` includes cache reads and
46
+ writes (Anthropic, Bedrock), `output_tokens` includes thinking tokens (Gemini), and
47
+ `total_tokens` is the vendor's total, or input + output when it reports none. When
48
+ streaming, `usage` arrives once, on the terminal delta.
49
+
50
+ ## Tool loops and `provider_metadata`
51
+
52
+ Some providers need opaque state from a response sent back on the next turn:
53
+ Gemini 3 rejects a replayed function call without its thought signature, and
54
+ Anthropic keeps its thinking blocks only if they are replayed verbatim.
55
+ `ToolCall.provider_metadata` and `AssistantMessage.provider_metadata` carry that
56
+ state, keyed by provider id (`{"gemini": {...}}`, `{"anthropic": {...}}`). Treat it
57
+ as opaque and copy it into the next request together with `content` and
58
+ `tool_calls`:
59
+
60
+ ```python
61
+ res = chat.complete(ChatInput(model=model, messages=msgs, tools=tools))
62
+ msgs.append(ChatMessage(role="assistant", content=res.message.content,
63
+ tool_calls=res.message.tool_calls,
64
+ provider_metadata=res.message.provider_metadata))
65
+ ```
66
+
67
+ When streaming, the state rides on `tool_call_delta.provider_metadata` (copy it
68
+ onto the `ToolCall` you assemble) and on the terminal delta's `provider_metadata`.
69
+ A connector reads only its own key, so a history survives a provider switch. If the
70
+ state is lost, Gemini falls back to Google's documented signature bypass, and
71
+ Anthropic rebuilds that turn and every later one without their thinking (Anthropic
72
+ invalidates thinking after a stripped turn), so the earlier reasoning is lost.
73
+
74
+ ## Providers
75
+
76
+ The **provider id selects the connector**; `ChatInput` / `ChatResult` are
77
+ identical across all of them.
78
+
79
+ **Chat (18):** the 15 first-class OpenAI-compatible providers —
80
+ `ProviderID.OPENAI`, `AZURE_OPENAI`, `OPENROUTER`, `GROQ`, `TOGETHER`,
81
+ `FIREWORKS`, `DEEPSEEK`, `XAI`, `MISTRAL`, `PERPLEXITY`, `DEEPINFRA`,
82
+ `CLOUDFLARE`, `VLLM`, `OLLAMA`, `LMSTUDIO` — all take `OpenAICompatConfig`; plus 3
83
+ natives: `ANTHROPIC` (`AnthropicConfig`), `BEDROCK` (`BedrockConfig`), `GEMINI`
84
+ (`GeminiConfig`).
85
+
86
+ **Embeddings (11):** the OpenAI-float subset — `OPENAI`, `AZURE_OPENAI`,
87
+ `OPENROUTER`, `TOGETHER`, `FIREWORKS`, `MISTRAL`, `DEEPINFRA`, `CLOUDFLARE`,
88
+ `VLLM`, `OLLAMA`, `LMSTUDIO`.
89
+
90
+ ```python
91
+ from thinwrap.llm import Embeddings, EmbeddingsInput, OpenAICompatConfig, ProviderID
92
+
93
+ emb = Embeddings(ProviderID.OPENAI, OpenAICompatConfig(api_key="..."))
94
+ out = emb.create(EmbeddingsInput(model="text-embedding-3-small", input=["hello", "world"]))
95
+ # out.embeddings is list[list[float]] in input order
96
+ ```
97
+
98
+ Per-provider details live in [`docs/providers/`](docs/providers/).
99
+
100
+ ## The ≥90% baseline & passthrough
101
+
102
+ Only fields ≥90% of providers support normalizably are first-class on
103
+ `ChatInput` / `ChatResult`; everything else rides through `Passthrough` (request)
104
+ / `raw` (response) and is **never emulated**:
105
+
106
+ ```python
107
+ from thinwrap.llm import Passthrough
108
+
109
+ res = chat.complete(ChatInput(model="...", messages=msgs,
110
+ passthrough=Passthrough(body={"logprobs": True})))
111
+ _ = res.raw # verbatim vendor body — reasoning CoT, cached-token counts, cost, ...
112
+ ```
113
+
114
+ ## Errors
115
+
116
+ Every failure raises `ConnectorError`: `provider_code` is one of `rate_limited`,
117
+ `auth_failed`, `provider_unavailable`, `invalid_request`,
118
+ `context_length_exceeded`, `content_filtered`, `unknown`. The raw `Retry-After`
119
+ rides in `cause["retryAfter"]` (+ parsed `cause["retryAfterSeconds"]`) — there is
120
+ no top-level retry attribute. `retryAfterSeconds` is set only for an integer delay
121
+ or an HTTP-date (`0` when the date has passed).
122
+
123
+ By HTTP status: `401`/`403` → `auth_failed`; `402` (out of credit) / `429` →
124
+ `rate_limited`; `408` / `5xx` → `provider_unavailable`; every other `4xx` →
125
+ `invalid_request`, unless the vendor's context-overflow or content-filter signal
126
+ matches. An error delivered inside a 2xx body or mid-stream keeps the HTTP status it
127
+ arrived with and is classified by its own code or type (`unknown` without one).
128
+
129
+ ## Bring your own transport
130
+
131
+ ```python
132
+ chat = Chat(ProviderID.OPENAI, OpenAICompatConfig(api_key="..."), transport=my_transport)
133
+ ```
134
+
135
+ `my_transport` implements `Transport` (`send(HttpRequest) -> HttpResponse`); add
136
+ an `open_stream` method for true incremental streaming (otherwise a buffered
137
+ fallback is used). The default stdlib `urllib` transport does both, never
138
+ follows redirects, and is HTTPS-only: the `http://` defaults of the local servers
139
+ (vLLM, Ollama, LM Studio) get an `invalid_request` telling you to bring your own
140
+ transport. Its socket timeout is 600 s per blocking read, so a long non-streaming
141
+ reasoning call is not cut off; pass `UrllibTransport(timeout=...)` for a different
142
+ budget. The wrapper is stateless — no caching, retries, or conversation memory.
143
+
144
+ ## Relationship to the siblings
145
+
146
+ One of four thinwrap llm libraries sharing a normalized surface and
147
+ byte-identical provider ids / error codes: TypeScript (`@thinwrap/llm`), PHP
148
+ (`thinwrap/llm`), Go (`thinwrap/llm-go`), Python (this package).
149
+
150
+ ## Security
151
+
152
+ Report vulnerabilities **privately** — please do not open a public issue. Preferred: a
153
+ [private security advisory](https://github.com/thinwrap/llm-py/security/advisories/new)
154
+ on this repository. Alternatively, email **security@thinwrap.dev**. Include the affected
155
+ versions and a minimal reproduction if you have one.
156
+
157
+ A vulnerability in a *provider's* own API or service belongs to that vendor rather than
158
+ to this wrapper — please report those upstream.
159
+
160
+ ## License
161
+
162
+ MIT © Dmitry Polyanovsky & contributors
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "thinwrap-llm"
7
+ version = "1.1.0"
8
+ description = "Lightweight, SDK-free, zero-dependency Python wrapper for chat completions (with streaming) and embeddings across 18 LLM providers behind one normalized surface."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Dmitry Polyanovsky" }]
14
+ keywords = ["llm", "ai", "chat", "embeddings", "openai", "anthropic", "bedrock", "gemini", "streaming"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Typing :: Typed",
18
+ ]
19
+ dependencies = []
20
+
21
+ [project.optional-dependencies]
22
+ dev = ["pytest>=8", "pytest-cov"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://thinwrap.dev"
26
+ Repository = "https://github.com/thinwrap/llm-py"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+ include = ["thinwrap.llm*"]
31
+ namespaces = true
32
+
33
+ # Explicit so the PEP 561 marker ships whatever setuptools the build resolves,
34
+ # rather than relying on a default.
35
+ [tool.setuptools.package-data]
36
+ "thinwrap.llm" = ["py.typed"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,51 @@
1
+ """thinwrap.llm — unified, SDK-free, zero-dependency Python wrapper for chat
2
+ completions (with streaming) and embeddings across 18 LLM providers behind one
3
+ normalized surface.
4
+
5
+ Switching vendor is a change of provider id + model only::
6
+
7
+ from thinwrap.llm import Chat, ChatInput, ChatMessage, OpenAICompatConfig, ProviderID
8
+
9
+ chat = Chat(ProviderID.OPENAI, OpenAICompatConfig(api_key="..."))
10
+ res = chat.complete(ChatInput(model="gpt-4o-mini",
11
+ messages=[ChatMessage(role="user", content="Hi")]))
12
+ print(res.message.content, res.usage.total_tokens)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from .chat import (
18
+ AssistantMessage,
19
+ Chat,
20
+ ChatInput,
21
+ ChatMessage,
22
+ ChatResult,
23
+ ChatStreamDelta,
24
+ ContentPart,
25
+ ReasoningOptions,
26
+ ResponseFormat,
27
+ ToolCall,
28
+ ToolCallDelta,
29
+ ToolChoice,
30
+ ToolDefinition,
31
+ Usage,
32
+ image_part,
33
+ text_part,
34
+ )
35
+ from .config import AnthropicConfig, BedrockConfig, GeminiConfig, OpenAICompatConfig
36
+ from .embeddings import Embeddings, EmbeddingsInput, EmbeddingsResult, EmbeddingsUsage
37
+ from .enums import ProviderID
38
+ from .errors import ConnectorError, ProviderCode
39
+ from .passthrough import Passthrough
40
+ from .transport import HttpRequest, HttpResponse, Transport, UrllibTransport
41
+
42
+ __all__ = [
43
+ "Chat", "Embeddings",
44
+ "ChatInput", "ChatMessage", "ChatResult", "ChatStreamDelta", "ContentPart",
45
+ "ToolCall", "ToolCallDelta", "ToolDefinition", "ToolChoice", "ResponseFormat",
46
+ "ReasoningOptions", "Usage", "AssistantMessage", "text_part", "image_part",
47
+ "EmbeddingsInput", "EmbeddingsResult", "EmbeddingsUsage",
48
+ "OpenAICompatConfig", "AnthropicConfig", "BedrockConfig", "GeminiConfig",
49
+ "ProviderID", "ConnectorError", "ProviderCode", "Passthrough",
50
+ "Transport", "UrllibTransport", "HttpRequest", "HttpResponse",
51
+ ]