paperllm 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.
paperllm-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bilal Zonjy, MD
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,214 @@
1
+ Metadata-Version: 2.4
2
+ Name: paperllm
3
+ Version: 0.1.0
4
+ Summary: Calling a local model about a paper, and keeping what the call cost: tool-submitted answers, truncation discipline, and a row per call.
5
+ Author: Bilal Zonjy, MD
6
+ License-Expression: MIT
7
+ Project-URL: Source, https://github.com/BilalZonjy/paperllm
8
+ Keywords: ollama,llm,pubmed,extraction,observability
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: ollama<1,>=0.5.1
17
+ Requires-Dist: pydantic<3,>=2
18
+ Requires-Dist: sqlalchemy<3,>=2
19
+ Provides-Extra: migrations
20
+ Requires-Dist: alembic<2,>=1.13; extra == "migrations"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # paperllm
26
+
27
+ Calling a local model about a paper, and keeping what the call cost.
28
+
29
+ - **Call shapes that survive a small model** — an agentic call whose answer arrives through a tool
30
+ rather than a `format=` grammar, a truncation check at every point a reply is returned, and a
31
+ thinking-versus-content split that finds the answer where a reasoning model actually put it.
32
+ - **A row per call** — tokens in and out, the window and budget it ran under, why generation
33
+ stopped, how long it took, and whether it worked.
34
+
35
+ Extracted from a SUDEP literature-review pipeline, where it runs a three-call extraction cascade over
36
+ ~2,500 papers. **v0.1.0, and honestly 0.x**: one consumer so far, which is why the public surface is
37
+ deliberately small (see *What it does not do*). Expect the API to move before 1.0; pin a version.
38
+
39
+ ## Two boundaries worth stating before you read further
40
+
41
+ **Ollama-shaped.** `done_reason`, the thinking channel, the retry semantics and the shape of a tool
42
+ round are Ollama's. The name says `llm`; the code says Ollama. A second backend would need more than
43
+ a new client object, and pretending otherwise here would be the kind of premature generality this
44
+ library was extracted specifically to avoid.
45
+
46
+ **Paper-scoped.** `llm_call.pmid` is a PubMed identifier, not a generic subject key. That is
47
+ deliberate — a sibling to `pubmedcorpus`, not a general-purpose LLM client.
48
+
49
+ ## Installing
50
+
51
+ ```bash
52
+ pip install paperllm
53
+ ```
54
+
55
+ Take the extra if you intend to run the migration that creates the call log's tables:
56
+
57
+ ```bash
58
+ pip install "paperllm[migrations]"
59
+ ```
60
+
61
+ **No database driver is installed for you.** Nothing here is Postgres-specific, so which driver to
62
+ use is your decision — but read the `now()` note under *The recorder* before choosing a primary-key
63
+ strategy on Postgres.
64
+
65
+ ## Using it
66
+
67
+ Everything the library needs to know about your deployment is one object:
68
+
69
+ ```python
70
+ from paperllm.caller import Caller
71
+ from paperllm.config import CallConfig
72
+
73
+ caller = Caller(CallConfig(
74
+ host="http://localhost:11434",
75
+ # (model, purpose) -> context window. A judgement about *your* ensemble: this model's VRAM
76
+ # ceiling, this kind of call's needs. The library only needs to be able to ask.
77
+ num_ctx_for=lambda model, purpose: 32768,
78
+ # Whether this model may emit reasoning tokens.
79
+ think_for=lambda model: True,
80
+ # Optional. Without it, nothing is written down and no database is needed.
81
+ recorder=None,
82
+ ))
83
+
84
+ answer = caller.extract(model, messages, MySchema, num_predict=16384)
85
+ ```
86
+
87
+ `purpose` is one of `paperllm.config.EXTRACTION | PROSE | AGENTIC` — the three kinds of call this
88
+ library makes. It is **opaque to the library**: handed to `num_ctx_for`, stored on the call record,
89
+ never branched on. You decide what each is worth in context tokens.
90
+
91
+ ### The calls
92
+
93
+ | | |
94
+ |---|---|
95
+ | `chat_raw` | one constrained-decoding call → raw content |
96
+ | `classify` / `extract` | the same, parsed and validated into a Pydantic model |
97
+ | `chat_prose` | unconstrained generation |
98
+ | `chat_agentic` | the model may call tools, you run them, it continues |
99
+ | `reason` | one agentic call whose answer arrives through a tool; returns `(scratchpad, was_truncated)` |
100
+
101
+ Two exceptions, and they are **siblings, not a hierarchy**: `ExtractionError` means the model
102
+ produced nothing usable; `Truncated` means generation stopped on `num_predict`. A caller that
103
+ swallows the first must decide about the second separately, because a budget bug filed as a
104
+ judgement about a paper is how a number ends up in the record as evidence.
105
+
106
+ `Truncated` carries both `content` and `thinking`. On a reasoning model the second is usually the
107
+ only one with anything in it — measured over one run, 31 of 48 tool-calling turns had empty
108
+ `content` — so a cut-off call is a shorter scratchpad rather than a lost paper.
109
+
110
+ ### Budgets are yours
111
+
112
+ `num_predict` defaults to `MIN_NUM_PREDICT` (2048), which is a **floor, not a working budget**.
113
+ Thinking tokens count against the budget, so a number sized for the answer alone leaves a reasoning
114
+ model with nothing left to answer with — 2048 is what "you forgot to choose" looks like, small
115
+ enough to catch.
116
+
117
+ The right number depends on how your prompt was built and how much room it reserved for the reply.
118
+ Only you know both. The pipeline this came from keeps its measured budgets in
119
+ `sudep/analysis/budgets.py`; every one of them cost a failed run to learn.
120
+
121
+ ## The call log
122
+
123
+ Two tables. `llm_call` is one row per call, append-only; `call_stage` is the vocabulary naming which
124
+ call in a cascade a row is.
125
+
126
+ ```
127
+ pmid model stage created_at <- the key
128
+ purpose label <- what kind of call, and which step
129
+ success error_type error_message
130
+ num_ctx num_predict <- what it ran under
131
+ prompt_tokens eval_tokens <- what it used
132
+ done_reason duration_ms
133
+ ```
134
+
135
+ **`stage` names which call this was, not how the paper ended.** A paper that fails call 1 and
136
+ succeeds on call 2 has no single outcome, but each of its calls has one. The questions worth asking
137
+ are `stage × success`, and a pmid with no successful row at any stage is the failed paper. Seed
138
+ `call_stage` with your own cascade; the shipped rows describe a three-call one.
139
+
140
+ `success` means the call produced what its stage was asked for — a well-formed reply carrying
141
+ unusable JSON is a failure, because the cascade treats it as one and the log has to agree with the
142
+ cascade rather than with HTTP.
143
+
144
+ ### The recorder, and the one trap
145
+
146
+ ```python
147
+ def recorder(call):
148
+ with your_own_session_scope() as session: # NOT the caller's session
149
+ paperllm.record.record(session, call)
150
+ ```
151
+
152
+ **It must not join the transaction of the work it is describing.** A failed extraction rolls back,
153
+ and a record of the failure written in that transaction rolls back with it — leaving a log that
154
+ holds exactly the calls that went well. Open a short-lived session per record and commit it.
155
+
156
+ That also makes the primary key safe: Postgres's `now()` is the *transaction* timestamp, so one row
157
+ per transaction is what keeps `created_at` distinct. If you batch, use `clock_timestamp()`.
158
+
159
+ A recorder that raises does not break the call — `record.emit` swallows it and logs at WARNING. A run
160
+ that finishes with an incomplete log beats a run that died protecting its bookkeeping.
161
+
162
+ ## It grows, and nothing prunes it
163
+
164
+ One row per call, and calls are never updated or replaced — a full pass over a few thousand papers
165
+ with a handful of models is tens of thousands of rows. **There is no retention policy, no TTL and no
166
+ cleanup job, deliberately.** A log that deletes on its own is a log you cannot trust to answer a
167
+ question about last month, and the only honest default for "how long is a call worth keeping" is
168
+ however long its answer stays interesting.
169
+
170
+ So deletion is an operator's decision, taken deliberately:
171
+
172
+ ```sql
173
+ -- What you are about to remove, before removing it.
174
+ SELECT date_trunc('month', created_at) AS month, count(*)
175
+ FROM llm_call GROUP BY 1 ORDER BY 1;
176
+
177
+ DELETE FROM llm_call WHERE created_at < '2026-01-01';
178
+ ```
179
+
180
+ `ix_llm_call_created_at` exists for exactly that scan.
181
+
182
+ **Age is the only axis available, and that is worth knowing before you rely on it.** The table
183
+ carries no schema or prompt version, so "delete everything from before the prompt changed" has to be
184
+ expressed as a date — look up when the version bumped and cut there. Storing the versions here was
185
+ rejected on the grounds that a call log should not have to be told what question the caller was
186
+ asking; if that turns out to be wrong, it is an added column and a migration, not a redesign.
187
+
188
+ Nothing downstream reads this table, so a delete cannot break a pipeline — only an analysis you had
189
+ not run yet.
190
+
191
+ ## Migrations
192
+
193
+ The library owns its schema and ships an Alembic branch labelled `paperllm`, creating both tables and
194
+ seeding the vocabulary in one revision — an unseeded lookup table would make the foreign key reject
195
+ every insert. Add it to your `version_locations`, then `alembic upgrade heads` (**`heads`**, not
196
+ `head`: with more than one branch, `head` is ambiguous and errors).
197
+
198
+ `paperllm.db.Base` has its own `MetaData`, deliberately: nothing here has a foreign key crossing into
199
+ your schema — `llm_call.pmid` names a pmid and carries none, so a deleted paper does not take the
200
+ record of its failures with it.
201
+
202
+ ## What it does not do
203
+
204
+ - **No CLI.** Deciding which environment variables must be present before touching a database is the
205
+ application's call.
206
+ - **No sessions.** `record` takes one you opened.
207
+ - **No environment reading.** Everything arrives on `CallConfig`.
208
+ - **No re-exports** from `__init__.py`. Import submodules, so the public surface stays small enough
209
+ to reshape once a second consumer shows where the joints actually belong.
210
+
211
+
212
+ Offline by construction: no Ollama, no database. `_ollama` is the single seam every call goes
213
+ through, and `CallConfig` is built in the test rather than patched onto a global — which is the point
214
+ of the window and thinking policies being callables.
@@ -0,0 +1,190 @@
1
+ # paperllm
2
+
3
+ Calling a local model about a paper, and keeping what the call cost.
4
+
5
+ - **Call shapes that survive a small model** — an agentic call whose answer arrives through a tool
6
+ rather than a `format=` grammar, a truncation check at every point a reply is returned, and a
7
+ thinking-versus-content split that finds the answer where a reasoning model actually put it.
8
+ - **A row per call** — tokens in and out, the window and budget it ran under, why generation
9
+ stopped, how long it took, and whether it worked.
10
+
11
+ Extracted from a SUDEP literature-review pipeline, where it runs a three-call extraction cascade over
12
+ ~2,500 papers. **v0.1.0, and honestly 0.x**: one consumer so far, which is why the public surface is
13
+ deliberately small (see *What it does not do*). Expect the API to move before 1.0; pin a version.
14
+
15
+ ## Two boundaries worth stating before you read further
16
+
17
+ **Ollama-shaped.** `done_reason`, the thinking channel, the retry semantics and the shape of a tool
18
+ round are Ollama's. The name says `llm`; the code says Ollama. A second backend would need more than
19
+ a new client object, and pretending otherwise here would be the kind of premature generality this
20
+ library was extracted specifically to avoid.
21
+
22
+ **Paper-scoped.** `llm_call.pmid` is a PubMed identifier, not a generic subject key. That is
23
+ deliberate — a sibling to `pubmedcorpus`, not a general-purpose LLM client.
24
+
25
+ ## Installing
26
+
27
+ ```bash
28
+ pip install paperllm
29
+ ```
30
+
31
+ Take the extra if you intend to run the migration that creates the call log's tables:
32
+
33
+ ```bash
34
+ pip install "paperllm[migrations]"
35
+ ```
36
+
37
+ **No database driver is installed for you.** Nothing here is Postgres-specific, so which driver to
38
+ use is your decision — but read the `now()` note under *The recorder* before choosing a primary-key
39
+ strategy on Postgres.
40
+
41
+ ## Using it
42
+
43
+ Everything the library needs to know about your deployment is one object:
44
+
45
+ ```python
46
+ from paperllm.caller import Caller
47
+ from paperllm.config import CallConfig
48
+
49
+ caller = Caller(CallConfig(
50
+ host="http://localhost:11434",
51
+ # (model, purpose) -> context window. A judgement about *your* ensemble: this model's VRAM
52
+ # ceiling, this kind of call's needs. The library only needs to be able to ask.
53
+ num_ctx_for=lambda model, purpose: 32768,
54
+ # Whether this model may emit reasoning tokens.
55
+ think_for=lambda model: True,
56
+ # Optional. Without it, nothing is written down and no database is needed.
57
+ recorder=None,
58
+ ))
59
+
60
+ answer = caller.extract(model, messages, MySchema, num_predict=16384)
61
+ ```
62
+
63
+ `purpose` is one of `paperllm.config.EXTRACTION | PROSE | AGENTIC` — the three kinds of call this
64
+ library makes. It is **opaque to the library**: handed to `num_ctx_for`, stored on the call record,
65
+ never branched on. You decide what each is worth in context tokens.
66
+
67
+ ### The calls
68
+
69
+ | | |
70
+ |---|---|
71
+ | `chat_raw` | one constrained-decoding call → raw content |
72
+ | `classify` / `extract` | the same, parsed and validated into a Pydantic model |
73
+ | `chat_prose` | unconstrained generation |
74
+ | `chat_agentic` | the model may call tools, you run them, it continues |
75
+ | `reason` | one agentic call whose answer arrives through a tool; returns `(scratchpad, was_truncated)` |
76
+
77
+ Two exceptions, and they are **siblings, not a hierarchy**: `ExtractionError` means the model
78
+ produced nothing usable; `Truncated` means generation stopped on `num_predict`. A caller that
79
+ swallows the first must decide about the second separately, because a budget bug filed as a
80
+ judgement about a paper is how a number ends up in the record as evidence.
81
+
82
+ `Truncated` carries both `content` and `thinking`. On a reasoning model the second is usually the
83
+ only one with anything in it — measured over one run, 31 of 48 tool-calling turns had empty
84
+ `content` — so a cut-off call is a shorter scratchpad rather than a lost paper.
85
+
86
+ ### Budgets are yours
87
+
88
+ `num_predict` defaults to `MIN_NUM_PREDICT` (2048), which is a **floor, not a working budget**.
89
+ Thinking tokens count against the budget, so a number sized for the answer alone leaves a reasoning
90
+ model with nothing left to answer with — 2048 is what "you forgot to choose" looks like, small
91
+ enough to catch.
92
+
93
+ The right number depends on how your prompt was built and how much room it reserved for the reply.
94
+ Only you know both. The pipeline this came from keeps its measured budgets in
95
+ `sudep/analysis/budgets.py`; every one of them cost a failed run to learn.
96
+
97
+ ## The call log
98
+
99
+ Two tables. `llm_call` is one row per call, append-only; `call_stage` is the vocabulary naming which
100
+ call in a cascade a row is.
101
+
102
+ ```
103
+ pmid model stage created_at <- the key
104
+ purpose label <- what kind of call, and which step
105
+ success error_type error_message
106
+ num_ctx num_predict <- what it ran under
107
+ prompt_tokens eval_tokens <- what it used
108
+ done_reason duration_ms
109
+ ```
110
+
111
+ **`stage` names which call this was, not how the paper ended.** A paper that fails call 1 and
112
+ succeeds on call 2 has no single outcome, but each of its calls has one. The questions worth asking
113
+ are `stage × success`, and a pmid with no successful row at any stage is the failed paper. Seed
114
+ `call_stage` with your own cascade; the shipped rows describe a three-call one.
115
+
116
+ `success` means the call produced what its stage was asked for — a well-formed reply carrying
117
+ unusable JSON is a failure, because the cascade treats it as one and the log has to agree with the
118
+ cascade rather than with HTTP.
119
+
120
+ ### The recorder, and the one trap
121
+
122
+ ```python
123
+ def recorder(call):
124
+ with your_own_session_scope() as session: # NOT the caller's session
125
+ paperllm.record.record(session, call)
126
+ ```
127
+
128
+ **It must not join the transaction of the work it is describing.** A failed extraction rolls back,
129
+ and a record of the failure written in that transaction rolls back with it — leaving a log that
130
+ holds exactly the calls that went well. Open a short-lived session per record and commit it.
131
+
132
+ That also makes the primary key safe: Postgres's `now()` is the *transaction* timestamp, so one row
133
+ per transaction is what keeps `created_at` distinct. If you batch, use `clock_timestamp()`.
134
+
135
+ A recorder that raises does not break the call — `record.emit` swallows it and logs at WARNING. A run
136
+ that finishes with an incomplete log beats a run that died protecting its bookkeeping.
137
+
138
+ ## It grows, and nothing prunes it
139
+
140
+ One row per call, and calls are never updated or replaced — a full pass over a few thousand papers
141
+ with a handful of models is tens of thousands of rows. **There is no retention policy, no TTL and no
142
+ cleanup job, deliberately.** A log that deletes on its own is a log you cannot trust to answer a
143
+ question about last month, and the only honest default for "how long is a call worth keeping" is
144
+ however long its answer stays interesting.
145
+
146
+ So deletion is an operator's decision, taken deliberately:
147
+
148
+ ```sql
149
+ -- What you are about to remove, before removing it.
150
+ SELECT date_trunc('month', created_at) AS month, count(*)
151
+ FROM llm_call GROUP BY 1 ORDER BY 1;
152
+
153
+ DELETE FROM llm_call WHERE created_at < '2026-01-01';
154
+ ```
155
+
156
+ `ix_llm_call_created_at` exists for exactly that scan.
157
+
158
+ **Age is the only axis available, and that is worth knowing before you rely on it.** The table
159
+ carries no schema or prompt version, so "delete everything from before the prompt changed" has to be
160
+ expressed as a date — look up when the version bumped and cut there. Storing the versions here was
161
+ rejected on the grounds that a call log should not have to be told what question the caller was
162
+ asking; if that turns out to be wrong, it is an added column and a migration, not a redesign.
163
+
164
+ Nothing downstream reads this table, so a delete cannot break a pipeline — only an analysis you had
165
+ not run yet.
166
+
167
+ ## Migrations
168
+
169
+ The library owns its schema and ships an Alembic branch labelled `paperllm`, creating both tables and
170
+ seeding the vocabulary in one revision — an unseeded lookup table would make the foreign key reject
171
+ every insert. Add it to your `version_locations`, then `alembic upgrade heads` (**`heads`**, not
172
+ `head`: with more than one branch, `head` is ambiguous and errors).
173
+
174
+ `paperllm.db.Base` has its own `MetaData`, deliberately: nothing here has a foreign key crossing into
175
+ your schema — `llm_call.pmid` names a pmid and carries none, so a deleted paper does not take the
176
+ record of its failures with it.
177
+
178
+ ## What it does not do
179
+
180
+ - **No CLI.** Deciding which environment variables must be present before touching a database is the
181
+ application's call.
182
+ - **No sessions.** `record` takes one you opened.
183
+ - **No environment reading.** Everything arrives on `CallConfig`.
184
+ - **No re-exports** from `__init__.py`. Import submodules, so the public surface stays small enough
185
+ to reshape once a second consumer shows where the joints actually belong.
186
+
187
+
188
+ Offline by construction: no Ollama, no database. `_ollama` is the single seam every call goes
189
+ through, and `CallConfig` is built in the test rather than patched onto a global — which is the point
190
+ of the window and thinking policies being callables.
@@ -0,0 +1,21 @@
1
+ """Local-model call discipline for reading papers, and a durable record of every call.
2
+
3
+ **Being extracted from the SUDEP review pipeline it was written for**, one step at a time, and it
4
+ lives inside `backend/` while that happens: `PYTHONPATH` is already `backend/`, so `import paperllm`
5
+ resolves with no install step. Same route `pubmedcorpus` took.
6
+
7
+ What it is for: the call shapes that survive a local model — an agentic call whose answer arrives
8
+ through a tool rather than a `format=` grammar, a truncation check at every point a reply is returned,
9
+ the thinking-versus-content split that decides where a reasoning model actually put its work — and a
10
+ table that keeps what each call cost, so budget and window questions are settled by measurement rather
11
+ than by arithmetic.
12
+
13
+ **Ollama-shaped, despite the name.** The retry semantics, `done_reason`, and the thinking channel are
14
+ Ollama's. A second backend would need more than a new client object.
15
+
16
+ **No re-exports here, deliberately.** Callers import submodules — `from paperllm import record` — so
17
+ the public surface stays small enough to reshape once a second consumer shows where the joints
18
+ actually belong.
19
+
20
+ **Postgres-only**, like its sibling: the recorder's write path is Postgres-specific.
21
+ """
@@ -0,0 +1,102 @@
1
+ """The object a consumer actually holds.
2
+
3
+ `client.py` is written as functions taking a `CallConfig` first, because that is what makes each one
4
+ testable on its own with a config built in the test. This binds one config to all of them, so a
5
+ caller constructs it once and passes *it* around instead of threading a config through every function
6
+ between the entry point and the model call.
7
+
8
+ Same shape as `pubmedcorpus.NCBIClient(config)`, and for the same reason: the configuration of a
9
+ long-lived service is stated once per run, and everything downstream reads it from the object rather
10
+ than assembling its own.
11
+
12
+ **Thin on purpose.** Every method is a one-line delegation, and the behaviour stays in `client.py`.
13
+ The alternative — moving the bodies in here — would bury four hundred lines of hard-won comments
14
+ inside a class for no gain, and would make each call path harder to exercise in isolation.
15
+ """
16
+
17
+ from typing import Type, TypeVar
18
+
19
+ from paperllm import client
20
+ from paperllm.config import CallConfig
21
+ from paperllm.record import CallContext
22
+ from pydantic import BaseModel
23
+
24
+ M = TypeVar("M", bound=BaseModel)
25
+
26
+
27
+ class Caller:
28
+ """One Ollama endpoint, one window policy, one place calls are recorded.
29
+
30
+ Construct it where the application knows its own configuration — for a CLI, once per command —
31
+ and hand it down. Cheap to build: the underlying `ollama.Client` is created lazily, per host, on
32
+ the first call that needs it.
33
+ """
34
+
35
+ def __init__(self, config: CallConfig):
36
+ self.config = config
37
+
38
+ # --- availability ---------------------------------------------------------------
39
+
40
+ def available_models(self) -> set[str]:
41
+ return client.available_models(self.config)
42
+
43
+ def require_models(self, models: list[str]) -> None:
44
+ return client.require_models(self.config, models)
45
+
46
+ # --- embeddings -----------------------------------------------------------------
47
+
48
+ def embed(self, model: str, texts: list[str]) -> list[list[float]]:
49
+ return client.embed(self.config, model, texts)
50
+
51
+ # --- constrained calls ----------------------------------------------------------
52
+
53
+ def chat_raw(
54
+ self, model: str, messages: list[dict], schema: dict,
55
+ num_predict: int = client.MIN_NUM_PREDICT,
56
+ ) -> str:
57
+ return client.chat_raw(self.config, model, messages, schema, num_predict=num_predict)
58
+
59
+ def classify(
60
+ self, model: str, messages: list[dict], schema: dict, model_cls: Type[M],
61
+ num_predict: int = client.MIN_NUM_PREDICT, call: CallContext | None = None,
62
+ ) -> M:
63
+ return client.classify(self.config, model, messages, schema, model_cls,
64
+ num_predict=num_predict, call=call)
65
+
66
+ def extract(
67
+ self, model: str, messages: list[dict], model_cls: Type[M],
68
+ num_predict: int = client.MIN_NUM_PREDICT, call: CallContext | None = None,
69
+ ) -> M:
70
+ return client.extract(self.config, model, messages, model_cls,
71
+ num_predict=num_predict, call=call)
72
+
73
+ # --- unconstrained calls --------------------------------------------------------
74
+
75
+ def chat_prose(
76
+ self, model: str, messages: list[dict], *,
77
+ num_predict: int = client.MIN_NUM_PREDICT, temperature: float = 0.4,
78
+ ) -> str:
79
+ return client.chat_prose(self.config, model, messages,
80
+ num_predict=num_predict, temperature=temperature)
81
+
82
+ def chat_agentic(
83
+ self, model: str, messages: list[dict], tools: list[dict], dispatch, *,
84
+ num_predict: int = client.MIN_NUM_PREDICT, temperature: float = 0.4,
85
+ max_calls: int = 8, stop_when=None, on_response=None,
86
+ call: CallContext | None = None,
87
+ ) -> str:
88
+ return client.chat_agentic(
89
+ self.config, model, messages, tools, dispatch, num_predict=num_predict,
90
+ temperature=temperature, max_calls=max_calls, stop_when=stop_when,
91
+ on_response=on_response, call=call,
92
+ )
93
+
94
+ def reason(
95
+ self, model: str, messages: list[dict], tools: list[dict], dispatch, stop_when=None,
96
+ num_predict: int = client.MIN_NUM_PREDICT, on_response=None,
97
+ call: CallContext | None = None,
98
+ ) -> tuple[str | None, bool]:
99
+ return client.reason(
100
+ self.config, model, messages, tools, dispatch, stop_when=stop_when,
101
+ num_predict=num_predict, on_response=on_response, call=call,
102
+ )