thinchat 0.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.
- thinchat-0.1.0/.gitignore +25 -0
- thinchat-0.1.0/LICENSE +21 -0
- thinchat-0.1.0/PKG-INFO +169 -0
- thinchat-0.1.0/README.ko.md +139 -0
- thinchat-0.1.0/README.md +141 -0
- thinchat-0.1.0/pyproject.toml +82 -0
- thinchat-0.1.0/src/thinchat/__init__.py +51 -0
- thinchat-0.1.0/src/thinchat/claude_client.py +139 -0
- thinchat-0.1.0/src/thinchat/client.py +225 -0
- thinchat-0.1.0/src/thinchat/errors.py +45 -0
- thinchat-0.1.0/src/thinchat/keys.py +30 -0
- thinchat-0.1.0/src/thinchat/openai_client.py +283 -0
- thinchat-0.1.0/src/thinchat/providers.py +68 -0
- thinchat-0.1.0/src/thinchat/py.typed +0 -0
- thinchat-0.1.0/src/thinchat/structured_output.py +65 -0
- thinchat-0.1.0/tests/__init__.py +0 -0
- thinchat-0.1.0/tests/fakes.py +321 -0
- thinchat-0.1.0/tests/test_claude_client.py +249 -0
- thinchat-0.1.0/tests/test_keys.py +27 -0
- thinchat-0.1.0/tests/test_openai_client.py +331 -0
- thinchat-0.1.0/tests/test_providers.py +135 -0
- thinchat-0.1.0/tests/test_structured_output.py +50 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# Tooling caches
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
|
|
16
|
+
# AI coding agents
|
|
17
|
+
CLAUDE.md
|
|
18
|
+
.claude/
|
|
19
|
+
AGENTS.md
|
|
20
|
+
AGENT.md
|
|
21
|
+
.codex/
|
|
22
|
+
GEMINI.md
|
|
23
|
+
.gemini/
|
|
24
|
+
|
|
25
|
+
dev/
|
thinchat-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 seokhoonj
|
|
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.
|
thinchat-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thinchat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A tiny, unified client for four LLM providers: claude, openai, gemini, ollama.
|
|
5
|
+
Project-URL: Homepage, https://github.com/seokhoonj/thinchat
|
|
6
|
+
Project-URL: Repository, https://github.com/seokhoonj/thinchat
|
|
7
|
+
Project-URL: Issues, https://github.com/seokhoonj/thinchat/issues
|
|
8
|
+
Author-email: seokhoonj <seokhoonj@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: claude,completion,embeddings,gemini,llm,ollama,openai
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: anthropic>=0.40
|
|
20
|
+
Requires-Dist: httpx>=0.23
|
|
21
|
+
Requires-Dist: openai>=1.30
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# thinchat
|
|
30
|
+
|
|
31
|
+
**English** | [한국어](README.ko.md)
|
|
32
|
+
|
|
33
|
+
A tiny, unified client for four LLM providers — **claude, openai, gemini, ollama**.
|
|
34
|
+
|
|
35
|
+
Name a provider, then call it. Every client offers completion — whole, streamed, or
|
|
36
|
+
JSON-structured — and, where the provider has one, embeddings, each with an async twin. No
|
|
37
|
+
gateway, no router, no cost tracking: just the calls, over the providers' own SDKs.
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
```mermaid
|
|
42
|
+
flowchart LR
|
|
43
|
+
M["make_client(provider)"] --> C["Client<br/>openai-compatible · or claude"]
|
|
44
|
+
C --> V["complete · stream · parse · embed<br/>(+ a-prefixed async twins)"]
|
|
45
|
+
V --> S{{"vendor SDK"}}
|
|
46
|
+
S -->|ok| O(["str · dict · list float · stream"])
|
|
47
|
+
S -->|"SDK / transport error"| E(["LLMError"])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`make_client` looks the provider up in one factory map: openai/gemini/ollama share a single
|
|
51
|
+
class over the openai SDK (they differ only in data); claude has its own over anthropic.
|
|
52
|
+
A call builds the request, hits the SDK, and either extracts the reply or maps the failure
|
|
53
|
+
to one `LLMError`.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pip install thinchat
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Both provider SDKs (openai and anthropic) come with it, so every provider works out of the
|
|
62
|
+
box; each is imported lazily the first time you construct its client.
|
|
63
|
+
|
|
64
|
+
## Use
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from thinchat import make_client
|
|
68
|
+
|
|
69
|
+
llm = make_client("claude") # key from CLAUDE_API_KEY
|
|
70
|
+
print(llm.complete("Say hi in one word."))
|
|
71
|
+
print(llm.complete("Name a color.", system="Answer in one word.")) # system= steers any verb
|
|
72
|
+
|
|
73
|
+
# Structured output: reply parsed into a JSON object. The schema steers generation
|
|
74
|
+
# but is not validated locally, so check the returned dict's fields yourself.
|
|
75
|
+
verdict = llm.parse(
|
|
76
|
+
"Is this an ad? 'Buy now, 50% off — order today'",
|
|
77
|
+
schema={"type": "object",
|
|
78
|
+
"properties": {"is_ad": {"type": "boolean"}, "reason": {"type": "string"}},
|
|
79
|
+
"required": ["is_ad"]},
|
|
80
|
+
)
|
|
81
|
+
print(verdict["is_ad"])
|
|
82
|
+
|
|
83
|
+
# Streaming.
|
|
84
|
+
for chunk in make_client("claude").stream("Count to five."):
|
|
85
|
+
print(chunk, end="")
|
|
86
|
+
|
|
87
|
+
# Embeddings (openai / gemini / ollama; Claude has none).
|
|
88
|
+
vectors = make_client("openai").embed(["hello", "world"])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Every verb has an async twin — `acomplete`, `astream`, `aparse`, `aembed`:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
llm = make_client("claude")
|
|
95
|
+
text = await llm.acomplete("Summarize in one line: ...")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Providers
|
|
99
|
+
|
|
100
|
+
| provider | key env | embeddings |
|
|
101
|
+
|----------|-------------------|------------|
|
|
102
|
+
| `claude` | `CLAUDE_API_KEY` | no |
|
|
103
|
+
| `openai` | `OPENAI_API_KEY` | yes |
|
|
104
|
+
| `gemini` | `GEMINI_API_KEY` | yes |
|
|
105
|
+
| `ollama` | none (local) | yes |
|
|
106
|
+
|
|
107
|
+
openai, gemini, and ollama speak the same OpenAI-compatible API, so one SDK serves all
|
|
108
|
+
three; only the base URL, key, and default models differ. Ollama runs locally
|
|
109
|
+
(`OLLAMA_HOST`, default `http://localhost:11434`) and needs no key.
|
|
110
|
+
|
|
111
|
+
`max_tokens` caps the reply length — `make_client("claude", max_tokens=8192)`. Anthropic
|
|
112
|
+
requires the field, so claude defaults to 4096; the OpenAI-compatible providers omit it unless
|
|
113
|
+
you pass one, letting the model decide.
|
|
114
|
+
|
|
115
|
+
Keys are read from the environment. Set them once in your shell profile (`~/.bashrc`,
|
|
116
|
+
`~/.zshrc`) so every session picks them up — thinchat is a library and never imposes a file
|
|
117
|
+
location of its own:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
export CLAUDE_API_KEY="sk-ant-..."
|
|
121
|
+
export OPENAI_API_KEY="sk-..."
|
|
122
|
+
export GEMINI_API_KEY="..." # ollama runs locally and needs no key
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or pass a key explicitly, which overrides the environment:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
llm = make_client("claude", api_key="sk-ant-...", model="claude-haiku-4-5-20251001")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Capabilities
|
|
132
|
+
|
|
133
|
+
A client whose provider lacks a capability raises `UnsupportedError`. The capabilities are
|
|
134
|
+
`completion`, `streaming`, `structured_output`, and `embeddings`; check first with `supports`:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
make_client("claude").supports("embeddings") # False
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Errors
|
|
141
|
+
|
|
142
|
+
Everything thinchat raises on purpose derives from `ThinchatError`, so one `except` handles
|
|
143
|
+
the package's failures:
|
|
144
|
+
|
|
145
|
+
- `UnknownProviderError` — the name isn't one of the four providers.
|
|
146
|
+
- `ProviderUnavailableError` — the provider's SDK isn't installed, or no API key is set.
|
|
147
|
+
- `UnsupportedError` — the provider lacks the capability (e.g. embeddings on Claude).
|
|
148
|
+
- `LLMError` — the API call failed, or the reply was empty or malformed.
|
|
149
|
+
|
|
150
|
+
## Lifecycle
|
|
151
|
+
|
|
152
|
+
A client holds an HTTP connection pool. For a one-off script you can ignore it; for a
|
|
153
|
+
server that builds a client per request, close it so connections do not leak — use it as a
|
|
154
|
+
context manager, or call `close()` / `aclose()`:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
with make_client("claude") as llm:
|
|
158
|
+
llm.complete("...") # sync: closes the pool on exit
|
|
159
|
+
|
|
160
|
+
async with make_client("claude") as llm:
|
|
161
|
+
await llm.acomplete("...") # async: closes the async pool too
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`close()` frees the sync pool; if you drove async verbs, release with `aclose()` or
|
|
165
|
+
`async with` so the async pool is closed as well.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# thinchat
|
|
2
|
+
|
|
3
|
+
[English](README.md) | **한국어**
|
|
4
|
+
|
|
5
|
+
네 개의 LLM provider — **claude, openai, gemini, ollama** — 를 위한 작고 통일된 클라이언트.
|
|
6
|
+
|
|
7
|
+
provider 이름만 대면 호출됩니다. 모든 클라이언트가 completion(전체·스트리밍·JSON 구조화)을
|
|
8
|
+
제공하고, provider가 지원하면 embeddings도 제공하며, 각각 async 짝이 있습니다. gateway도,
|
|
9
|
+
router도, 비용 추적도 없이 — provider 자신의 SDK 위에서 호출만 합니다.
|
|
10
|
+
|
|
11
|
+
## 동작 방식
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
flowchart LR
|
|
15
|
+
M["make_client(provider)"] --> C["Client<br/>openai-compatible · or claude"]
|
|
16
|
+
C --> V["complete · stream · parse · embed<br/>(+ a-prefixed async twins)"]
|
|
17
|
+
V --> S{{"vendor SDK"}}
|
|
18
|
+
S -->|ok| O(["str · dict · list float · stream"])
|
|
19
|
+
S -->|"SDK / transport error"| E(["LLMError"])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`make_client`는 provider를 하나의 factory map에서 찾습니다: openai/gemini/ollama는 openai
|
|
23
|
+
SDK 위의 단일 클래스를 공유하고(데이터만 다름), claude는 anthropic 위에 자기 것을 둡니다.
|
|
24
|
+
호출은 요청을 조립해 SDK를 치고, 응답을 추출하거나 실패를 하나의 `LLMError`로 매핑합니다.
|
|
25
|
+
|
|
26
|
+
## 설치
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install thinchat
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
두 provider SDK(openai와 anthropic)가 함께 설치되어 모든 provider가 바로 동작합니다. 각 SDK는
|
|
33
|
+
해당 클라이언트를 처음 생성할 때 lazy하게 import됩니다.
|
|
34
|
+
|
|
35
|
+
## 사용
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from thinchat import make_client
|
|
39
|
+
|
|
40
|
+
llm = make_client("claude") # 키는 CLAUDE_API_KEY에서
|
|
41
|
+
print(llm.complete("Say hi in one word."))
|
|
42
|
+
print(llm.complete("Name a color.", system="Answer in one word.")) # system=은 모든 verb를 유도
|
|
43
|
+
|
|
44
|
+
# 구조화 출력: 응답을 JSON 객체로 파싱. schema는 생성을 유도할 뿐 로컬에서 검증하지 않으므로,
|
|
45
|
+
# 반환된 dict의 필드는 직접 확인하세요.
|
|
46
|
+
verdict = llm.parse(
|
|
47
|
+
"Is this an ad? 'Buy now, 50% off — order today'",
|
|
48
|
+
schema={"type": "object",
|
|
49
|
+
"properties": {"is_ad": {"type": "boolean"}, "reason": {"type": "string"}},
|
|
50
|
+
"required": ["is_ad"]},
|
|
51
|
+
)
|
|
52
|
+
print(verdict["is_ad"])
|
|
53
|
+
|
|
54
|
+
# 스트리밍.
|
|
55
|
+
for chunk in make_client("claude").stream("Count to five."):
|
|
56
|
+
print(chunk, end="")
|
|
57
|
+
|
|
58
|
+
# 임베딩 (openai / gemini / ollama; Claude는 없음).
|
|
59
|
+
vectors = make_client("openai").embed(["hello", "world"])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
모든 verb에는 async 짝이 있습니다 — `acomplete`, `astream`, `aparse`, `aembed`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
llm = make_client("claude")
|
|
66
|
+
text = await llm.acomplete("Summarize in one line: ...")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Provider
|
|
70
|
+
|
|
71
|
+
| provider | 키 환경변수 | embeddings |
|
|
72
|
+
|----------|-------------------|------------|
|
|
73
|
+
| `claude` | `CLAUDE_API_KEY` | 아니오 |
|
|
74
|
+
| `openai` | `OPENAI_API_KEY` | 예 |
|
|
75
|
+
| `gemini` | `GEMINI_API_KEY` | 예 |
|
|
76
|
+
| `ollama` | 없음 (로컬) | 예 |
|
|
77
|
+
|
|
78
|
+
openai, gemini, ollama는 동일한 OpenAI 호환 API를 쓰므로 하나의 SDK가 셋을 다 처리하고, base
|
|
79
|
+
URL·키·기본 모델만 다릅니다. Ollama는 로컬에서 실행되며(`OLLAMA_HOST`, 기본
|
|
80
|
+
`http://localhost:11434`) 키가 필요 없습니다.
|
|
81
|
+
|
|
82
|
+
`max_tokens`는 응답 길이의 상한입니다 — `make_client("claude", max_tokens=8192)`. Anthropic은
|
|
83
|
+
이 필드를 요구하므로 claude는 기본값 4096을 쓰고, OpenAI 호환 provider들은 값을 주지 않으면
|
|
84
|
+
생략해 모델이 정하게 둡니다.
|
|
85
|
+
|
|
86
|
+
키는 환경에서 읽습니다. 셸 프로파일(`~/.bashrc`, `~/.zshrc`)에 한 번 넣어두면 모든 세션이
|
|
87
|
+
인식합니다 — thinchat은 라이브러리라 자체 파일 위치를 강제하지 않습니다:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
export CLAUDE_API_KEY="sk-ant-..."
|
|
91
|
+
export OPENAI_API_KEY="sk-..."
|
|
92
|
+
export GEMINI_API_KEY="..." # ollama는 로컬이라 키 불필요
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
또는 환경변수를 덮어쓰며 키를 직접 넘길 수도 있습니다:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
llm = make_client("claude", api_key="sk-ant-...", model="claude-haiku-4-5-20251001")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 지원 기능(Capabilities)
|
|
102
|
+
|
|
103
|
+
provider가 지원하지 않는 기능을 호출하면 `UnsupportedError`가 발생합니다. 기능은 `completion`,
|
|
104
|
+
`streaming`, `structured_output`, `embeddings`이며, `supports`로 먼저 확인하세요:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
make_client("claude").supports("embeddings") # False
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 에러
|
|
111
|
+
|
|
112
|
+
thinchat이 의도적으로 던지는 모든 에러는 `ThinchatError`에서 파생되므로, 하나의 `except`로 이
|
|
113
|
+
패키지의 실패를 처리할 수 있습니다:
|
|
114
|
+
|
|
115
|
+
- `UnknownProviderError` — 이름이 네 provider 중 하나가 아님.
|
|
116
|
+
- `ProviderUnavailableError` — provider의 SDK가 설치되지 않았거나, API 키가 없음.
|
|
117
|
+
- `UnsupportedError` — provider가 그 기능을 지원하지 않음(예: Claude의 embeddings).
|
|
118
|
+
- `LLMError` — API 호출이 실패했거나, 응답이 비었거나 형식이 잘못됨.
|
|
119
|
+
|
|
120
|
+
## 생명주기(Lifecycle)
|
|
121
|
+
|
|
122
|
+
클라이언트는 HTTP 연결 풀을 보유합니다. 일회성 스크립트라면 신경 쓰지 않아도 되지만, 요청마다
|
|
123
|
+
클라이언트를 만드는 서버라면 연결이 새지 않도록 닫아야 합니다 — context manager로 쓰거나
|
|
124
|
+
`close()` / `aclose()`를 호출하세요:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
with make_client("claude") as llm:
|
|
128
|
+
llm.complete("...") # 동기: 종료 시 풀을 닫음
|
|
129
|
+
|
|
130
|
+
async with make_client("claude") as llm:
|
|
131
|
+
await llm.acomplete("...") # 비동기: async 풀도 닫음
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`close()`는 동기 풀을 해제합니다. async verb를 사용했다면 `aclose()`나 `async with`로 async
|
|
135
|
+
풀까지 닫으세요.
|
|
136
|
+
|
|
137
|
+
## 라이선스
|
|
138
|
+
|
|
139
|
+
MIT
|
thinchat-0.1.0/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# thinchat
|
|
2
|
+
|
|
3
|
+
**English** | [한국어](README.ko.md)
|
|
4
|
+
|
|
5
|
+
A tiny, unified client for four LLM providers — **claude, openai, gemini, ollama**.
|
|
6
|
+
|
|
7
|
+
Name a provider, then call it. Every client offers completion — whole, streamed, or
|
|
8
|
+
JSON-structured — and, where the provider has one, embeddings, each with an async twin. No
|
|
9
|
+
gateway, no router, no cost tracking: just the calls, over the providers' own SDKs.
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
flowchart LR
|
|
15
|
+
M["make_client(provider)"] --> C["Client<br/>openai-compatible · or claude"]
|
|
16
|
+
C --> V["complete · stream · parse · embed<br/>(+ a-prefixed async twins)"]
|
|
17
|
+
V --> S{{"vendor SDK"}}
|
|
18
|
+
S -->|ok| O(["str · dict · list float · stream"])
|
|
19
|
+
S -->|"SDK / transport error"| E(["LLMError"])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`make_client` looks the provider up in one factory map: openai/gemini/ollama share a single
|
|
23
|
+
class over the openai SDK (they differ only in data); claude has its own over anthropic.
|
|
24
|
+
A call builds the request, hits the SDK, and either extracts the reply or maps the failure
|
|
25
|
+
to one `LLMError`.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install thinchat
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Both provider SDKs (openai and anthropic) come with it, so every provider works out of the
|
|
34
|
+
box; each is imported lazily the first time you construct its client.
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from thinchat import make_client
|
|
40
|
+
|
|
41
|
+
llm = make_client("claude") # key from CLAUDE_API_KEY
|
|
42
|
+
print(llm.complete("Say hi in one word."))
|
|
43
|
+
print(llm.complete("Name a color.", system="Answer in one word.")) # system= steers any verb
|
|
44
|
+
|
|
45
|
+
# Structured output: reply parsed into a JSON object. The schema steers generation
|
|
46
|
+
# but is not validated locally, so check the returned dict's fields yourself.
|
|
47
|
+
verdict = llm.parse(
|
|
48
|
+
"Is this an ad? 'Buy now, 50% off — order today'",
|
|
49
|
+
schema={"type": "object",
|
|
50
|
+
"properties": {"is_ad": {"type": "boolean"}, "reason": {"type": "string"}},
|
|
51
|
+
"required": ["is_ad"]},
|
|
52
|
+
)
|
|
53
|
+
print(verdict["is_ad"])
|
|
54
|
+
|
|
55
|
+
# Streaming.
|
|
56
|
+
for chunk in make_client("claude").stream("Count to five."):
|
|
57
|
+
print(chunk, end="")
|
|
58
|
+
|
|
59
|
+
# Embeddings (openai / gemini / ollama; Claude has none).
|
|
60
|
+
vectors = make_client("openai").embed(["hello", "world"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Every verb has an async twin — `acomplete`, `astream`, `aparse`, `aembed`:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
llm = make_client("claude")
|
|
67
|
+
text = await llm.acomplete("Summarize in one line: ...")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Providers
|
|
71
|
+
|
|
72
|
+
| provider | key env | embeddings |
|
|
73
|
+
|----------|-------------------|------------|
|
|
74
|
+
| `claude` | `CLAUDE_API_KEY` | no |
|
|
75
|
+
| `openai` | `OPENAI_API_KEY` | yes |
|
|
76
|
+
| `gemini` | `GEMINI_API_KEY` | yes |
|
|
77
|
+
| `ollama` | none (local) | yes |
|
|
78
|
+
|
|
79
|
+
openai, gemini, and ollama speak the same OpenAI-compatible API, so one SDK serves all
|
|
80
|
+
three; only the base URL, key, and default models differ. Ollama runs locally
|
|
81
|
+
(`OLLAMA_HOST`, default `http://localhost:11434`) and needs no key.
|
|
82
|
+
|
|
83
|
+
`max_tokens` caps the reply length — `make_client("claude", max_tokens=8192)`. Anthropic
|
|
84
|
+
requires the field, so claude defaults to 4096; the OpenAI-compatible providers omit it unless
|
|
85
|
+
you pass one, letting the model decide.
|
|
86
|
+
|
|
87
|
+
Keys are read from the environment. Set them once in your shell profile (`~/.bashrc`,
|
|
88
|
+
`~/.zshrc`) so every session picks them up — thinchat is a library and never imposes a file
|
|
89
|
+
location of its own:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
export CLAUDE_API_KEY="sk-ant-..."
|
|
93
|
+
export OPENAI_API_KEY="sk-..."
|
|
94
|
+
export GEMINI_API_KEY="..." # ollama runs locally and needs no key
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or pass a key explicitly, which overrides the environment:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
llm = make_client("claude", api_key="sk-ant-...", model="claude-haiku-4-5-20251001")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Capabilities
|
|
104
|
+
|
|
105
|
+
A client whose provider lacks a capability raises `UnsupportedError`. The capabilities are
|
|
106
|
+
`completion`, `streaming`, `structured_output`, and `embeddings`; check first with `supports`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
make_client("claude").supports("embeddings") # False
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Errors
|
|
113
|
+
|
|
114
|
+
Everything thinchat raises on purpose derives from `ThinchatError`, so one `except` handles
|
|
115
|
+
the package's failures:
|
|
116
|
+
|
|
117
|
+
- `UnknownProviderError` — the name isn't one of the four providers.
|
|
118
|
+
- `ProviderUnavailableError` — the provider's SDK isn't installed, or no API key is set.
|
|
119
|
+
- `UnsupportedError` — the provider lacks the capability (e.g. embeddings on Claude).
|
|
120
|
+
- `LLMError` — the API call failed, or the reply was empty or malformed.
|
|
121
|
+
|
|
122
|
+
## Lifecycle
|
|
123
|
+
|
|
124
|
+
A client holds an HTTP connection pool. For a one-off script you can ignore it; for a
|
|
125
|
+
server that builds a client per request, close it so connections do not leak — use it as a
|
|
126
|
+
context manager, or call `close()` / `aclose()`:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
with make_client("claude") as llm:
|
|
130
|
+
llm.complete("...") # sync: closes the pool on exit
|
|
131
|
+
|
|
132
|
+
async with make_client("claude") as llm:
|
|
133
|
+
await llm.acomplete("...") # async: closes the async pool too
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`close()` frees the sync pool; if you drove async verbs, release with `aclose()` or
|
|
137
|
+
`async with` so the async pool is closed as well.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "thinchat"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A tiny, unified client for four LLM providers: claude, openai, gemini, ollama."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "seokhoonj", email = "seokhoonj@gmail.com" }]
|
|
13
|
+
keywords = ["llm", "openai", "claude", "gemini", "ollama", "completion", "embeddings"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
]
|
|
22
|
+
# 3.11+ for the typing niceties this package uses (e.g. Self).
|
|
23
|
+
requires-python = ">=3.11"
|
|
24
|
+
# openai, gemini, and ollama speak the OpenAI-compatible API, so the openai SDK powers
|
|
25
|
+
# three providers; claude uses the anthropic SDK. Both ship with the base install, so a
|
|
26
|
+
# plain `pip install thinchat` is ready for every provider -- each SDK is still imported
|
|
27
|
+
# lazily, only when its client is constructed. httpx is named directly (its transport
|
|
28
|
+
# errors are caught on the streaming path), so it is a direct dependency, not a transitive.
|
|
29
|
+
dependencies = [
|
|
30
|
+
"openai>=1.30",
|
|
31
|
+
"anthropic>=0.40",
|
|
32
|
+
"httpx>=0.23",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/seokhoonj/thinchat"
|
|
37
|
+
Repository = "https://github.com/seokhoonj/thinchat"
|
|
38
|
+
Issues = "https://github.com/seokhoonj/thinchat/issues"
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
# Development only; the provider SDKs are base dependencies, installed with the package.
|
|
42
|
+
dev = [
|
|
43
|
+
"mypy>=1.11",
|
|
44
|
+
"pytest>=8.0",
|
|
45
|
+
"pytest-asyncio>=0.23",
|
|
46
|
+
"ruff>=0.6",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/thinchat"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py311"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
58
|
+
ignore = ["E501"]
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.11"
|
|
62
|
+
files = ["src", "tests"]
|
|
63
|
+
strict = true
|
|
64
|
+
|
|
65
|
+
# The provider SDKs are optional extras, imported lazily and absent from a minimal
|
|
66
|
+
# environment, so mypy cannot see them -- and must not need to: the package type-checks
|
|
67
|
+
# without them.
|
|
68
|
+
[[tool.mypy.overrides]]
|
|
69
|
+
module = ["openai", "openai.*", "anthropic", "anthropic.*"]
|
|
70
|
+
ignore_missing_imports = true
|
|
71
|
+
|
|
72
|
+
# Tests get the real checks but not the annotation ceremony: `-> None` on every test
|
|
73
|
+
# function is noise, and a test's signature is never the thing under review.
|
|
74
|
+
[[tool.mypy.overrides]]
|
|
75
|
+
module = "tests.*"
|
|
76
|
+
disallow_untyped_defs = false
|
|
77
|
+
disallow_incomplete_defs = false
|
|
78
|
+
disallow_untyped_calls = false
|
|
79
|
+
check_untyped_defs = true
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""thinchat: a tiny, unified client for four LLM providers -- claude, openai, gemini, ollama.
|
|
2
|
+
|
|
3
|
+
Name a provider, then call it. Each client offers completion (whole, streamed, or
|
|
4
|
+
JSON-structured) and, where the provider has one, embeddings -- with an async twin for each.
|
|
5
|
+
|
|
6
|
+
from thinchat import make_client
|
|
7
|
+
|
|
8
|
+
llm = make_client("claude") # key from CLAUDE_API_KEY
|
|
9
|
+
print(llm.complete("Say hi in one word."))
|
|
10
|
+
verdict = llm.parse("Is this an ad? 'Buy now, 50% off'",
|
|
11
|
+
schema={"type": "object",
|
|
12
|
+
"properties": {"is_ad": {"type": "boolean"}},
|
|
13
|
+
"required": ["is_ad"]})
|
|
14
|
+
|
|
15
|
+
Installing thinchat brings both provider SDKs (openai and anthropic), so every provider
|
|
16
|
+
works out of the box; each SDK is imported lazily when you first construct its client.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
20
|
+
|
|
21
|
+
from thinchat.claude_client import ClaudeClient
|
|
22
|
+
from thinchat.client import Capability, Client, Provider
|
|
23
|
+
from thinchat.errors import (
|
|
24
|
+
LLMError,
|
|
25
|
+
ProviderUnavailableError,
|
|
26
|
+
ThinchatError,
|
|
27
|
+
UnknownProviderError,
|
|
28
|
+
UnsupportedError,
|
|
29
|
+
)
|
|
30
|
+
from thinchat.openai_client import OpenAICompatibleClient
|
|
31
|
+
from thinchat.providers import PROVIDERS, make_client
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"PROVIDERS",
|
|
35
|
+
"Capability",
|
|
36
|
+
"ClaudeClient",
|
|
37
|
+
"Client",
|
|
38
|
+
"LLMError",
|
|
39
|
+
"OpenAICompatibleClient",
|
|
40
|
+
"Provider",
|
|
41
|
+
"ProviderUnavailableError",
|
|
42
|
+
"ThinchatError",
|
|
43
|
+
"UnknownProviderError",
|
|
44
|
+
"UnsupportedError",
|
|
45
|
+
"make_client",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
__version__ = version("thinchat") # single source of truth: the installed metadata
|
|
50
|
+
except PackageNotFoundError: # running from a source tree that was never installed
|
|
51
|
+
__version__ = "0.0.0+unknown"
|