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.
@@ -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.
@@ -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
@@ -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"