Skip to content

Commit 7bdf521

Browse files
committed
call locations fixed
1 parent 28483e1 commit 7bdf521

17 files changed

+249
-67
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,8 @@ cython_debug/
266266
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
267267
# and can be added to the global gitignore or merged into this file. For a more nuclear
268268
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
269-
#.idea/
269+
.idea/
270270

271+
todo
271272

272273
covenance/_version.py

covenance/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
from .client import (
99
Covenance,
1010
ask_llm,
11-
get_default_client,
1211
llm_consensus,
13-
set_rate_limiter_verbose,
1412
)
1513
from .record import (
1614
Record,
@@ -27,8 +25,6 @@
2725
"ask_llm",
2826
"llm_consensus",
2927
"Covenance",
30-
"get_default_client",
31-
"set_rate_limiter_verbose",
3228
# Call records
3329
"Record",
3430
"get_records",

covenance/_version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
commit_id: COMMIT_ID
2929
__commit_id__: COMMIT_ID
3030

31-
__version__ = version = '0.0.3.dev5+ge86644923.d20260127'
32-
__version_tuple__ = version_tuple = (0, 0, 3, 'dev5', 'ge86644923.d20260127')
31+
__version__ = version = '0.0.3.dev12+g68cbf2a1f.d20260128'
32+
__version_tuple__ = version_tuple = (0, 0, 3, 'dev12', 'g68cbf2a1f.d20260128')
3333

3434
__commit_id__ = commit_id = None

covenance/client.py

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,10 @@
3030
get_openrouter_api_key,
3131
require_api_key,
3232
)
33-
from .record import Record, RecordStore, get_env_records_dir
33+
from .record import Record, RecordStore, capture_caller_context, copy_context, get_env_records_dir
3434
from .response_adapter import ResponseTypeAdapter
3535

3636

37-
def set_rate_limiter_verbose(verbose: bool) -> None:
38-
"""Enable or disable verbose logging for all rate limiters."""
39-
from .clients.anthropic_client import (
40-
set_rate_limiter_verbose as set_anthropic_verbose,
41-
)
42-
from .clients.google_client import set_rate_limiter_verbose as set_gemini_verbose
43-
from .clients.mistral_client import set_rate_limiter_verbose as set_mistral_verbose
44-
from .clients.openai_client import set_rate_limiter_verbose as set_openai_verbose
45-
46-
set_anthropic_verbose(verbose)
47-
set_gemini_verbose(verbose)
48-
set_openai_verbose(verbose)
49-
set_mistral_verbose(verbose)
50-
51-
5237
class Covenance:
5338
"""LLM client with isolated API keys and call records.
5439
@@ -192,18 +177,23 @@ def ask_llm[T](
192177
sys_msg: str | None = None,
193178
*,
194179
max_parsing_retries: int = 2,
180+
temperature: float | None = None,
195181
) -> T:
196-
"""Route to appropriate provider and make LLM call.
182+
"""Route to appropriate provider and make LLM call with given reponse type.
197183
198184
Args:
199185
user_msg: User message/prompt
200186
model: Model name - determines provider routing
201187
response_type: Type for structured output. Can be:
202188
- None or str: returns plain text
203189
- Pydantic model: returns model instance
204-
- list[X], tuple[...], etc.: wraps in Pydantic, then unwraps
190+
- int, bool, float, list[X], tuple[...] - simple python types
205191
sys_msg: Optional system message
206192
max_parsing_retries: Retries for structured output parsing errors
193+
temperature: Sampling temperature. None uses provider default.
194+
Range varies by provider (Anthropic: 0-1, others: 0-2).
195+
Note: temperature=0 aims for determinism but doesn't guarantee it
196+
due to GPU floating-point non-determinism and backend variability.
207197
"""
208198
provider = self._get_provider(model)
209199
client = self._get_client(provider)
@@ -239,6 +229,7 @@ def ask_llm[T](
239229
model=model,
240230
client_override=client,
241231
record_store=self._record_store,
232+
temperature=temperature,
242233
)
243234
if llm_type not in (None, str):
244235
try:
@@ -277,6 +268,9 @@ def llm_consensus[T](
277268
integration_model: Model for integration (defaults to same as model)
278269
parallel: Whether to make calls in parallel (default: True)
279270
"""
271+
# Capture caller info before any calls (especially before spawning threads)
272+
capture_caller_context()
273+
280274
if num_candidates == 1:
281275
return self.ask_llm(
282276
user_msg=user_msg,
@@ -302,8 +296,9 @@ def make_candidate_call(call_index: int) -> T:
302296
candidates: list[T] = []
303297
if parallel:
304298
with ThreadPoolExecutor(max_workers=num_candidates) as executor:
299+
# Each thread needs its own context copy
305300
futures = [
306-
executor.submit(make_candidate_call, i)
301+
executor.submit(copy_context().run, make_candidate_call, i)
307302
for i in range(num_candidates)
308303
]
309304
for future in as_completed(futures):
@@ -386,24 +381,23 @@ def print_usage(self, title: str | None = None, cost_format: str = "plain") -> N
386381
_default_client = Covenance(label="default client", records_dir=get_env_records_dir())
387382

388383

389-
def get_default_client() -> Covenance:
390-
return _default_client
391-
392-
393384
def ask_llm[T](
394385
user_msg: str,
395386
model: str,
396387
response_type: type[T] | None = None,
397388
sys_msg: str | None = None,
398389
*,
399390
max_parsing_retries: int = 2,
391+
temperature: float | None = None,
400392
) -> T:
393+
"""See docstring in the class method."""
401394
return _default_client.ask_llm(
402395
user_msg=user_msg,
403396
model=model,
404397
response_type=response_type,
405398
sys_msg=sys_msg,
406399
max_parsing_retries=max_parsing_retries,
400+
temperature=temperature,
407401
)
408402

409403

@@ -418,6 +412,7 @@ def llm_consensus[T](
418412
integration_model: str | None = None,
419413
parallel: bool = True,
420414
) -> T:
415+
"""See docstring in the class method."""
421416
return _default_client.llm_consensus(
422417
user_msg=user_msg,
423418
model=model,

covenance/clients/anthropic_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def ask_anthropic[T](
9797
*,
9898
client_override: Anthropic | None = None,
9999
record_store: "RecordStore | None" = None,
100+
temperature: float | None = None,
100101
) -> T:
101102
"""Call Anthropic API with structured output using tools parameter.
102103
@@ -171,6 +172,9 @@ def ask_anthropic[T](
171172
if sys_msg is not None:
172173
api_kwargs["system"] = sys_msg
173174

175+
if temperature is not None:
176+
api_kwargs["temperature"] = temperature
177+
174178
response = api_client.messages.create(**api_kwargs)
175179

176180
ended_at = datetime.now(UTC) # Record absolute end time

covenance/clients/google_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def ask_gemini[T](
101101
*,
102102
client_override: genai.Client | None = None,
103103
record_store: "RecordStore | None" = None,
104+
temperature: float | None = None,
104105
) -> T:
105106
"""Call Gemini API with automatic retry on rate limit errors.
106107
@@ -132,6 +133,9 @@ def ask_gemini[T](
132133
if sys_msg:
133134
cfg["system_instruction"] = sys_msg
134135

136+
if temperature is not None:
137+
cfg["temperature"] = temperature
138+
135139
for attempt in range(max_attempts):
136140
try:
137141
if VERBOSE and attempt > 0:

covenance/clients/grok_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def ask_grok[T](
3131
*,
3232
client_override: OpenAI | None = None,
3333
record_store: "RecordStore | None" = None,
34+
temperature: float | None = None,
3435
) -> T:
3536
"""Call xAI Grok API with automatic retry."""
3637
api_client = client_override or client
@@ -42,4 +43,5 @@ def ask_grok[T](
4243
model=model,
4344
provider="grok",
4445
record_store=record_store,
46+
temperature=temperature,
4547
)

covenance/clients/mistral_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def ask_mistral[T](
7878
*,
7979
client_override: Mistral | None = None,
8080
record_store: "RecordStore | None" = None,
81+
temperature: float | None = None,
8182
) -> T:
8283
"""Call Mistral API with structured output using native parse method.
8384
@@ -124,14 +125,15 @@ def ask_mistral[T](
124125
response = api_client.chat.complete(
125126
model=model,
126127
messages=messages,
128+
temperature=temperature,
127129
)
128130
else:
129131
# Use native structured output via chat.parse
130132
response = api_client.chat.parse(
131133
model=model,
132134
messages=messages,
133135
response_format=response_type,
134-
# temperature=0,
136+
temperature=temperature,
135137
)
136138

137139
ended_at = datetime.now(UTC) # Record absolute end time

covenance/clients/openai_client.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import re
24
import time
35
from datetime import UTC, datetime
@@ -9,10 +11,9 @@
911
from covenance.exceptions import StructuredOutputParsingError
1012
from covenance.keys import get_openai_api_key, require_api_key
1113
from covenance.models import OpenAIModels
12-
from covenance.record import TokenUsage
1314

1415
if TYPE_CHECKING:
15-
from covenance.record import RecordStore
16+
from covenance.record import RecordStore, TokenUsage
1617

1718
T = TypeVar("T")
1819

@@ -68,6 +69,8 @@ def _extract_openai_compatible_usage(
6869
response, model: str, provider: str = "openai"
6970
) -> TokenUsage:
7071
"""Extract token usage from OpenAI-compatible response."""
72+
from covenance.record import TokenUsage
73+
7174
if not hasattr(response, "usage") or response.usage is None:
7275
p_name = "OpenAI" if provider == "openai" else provider.capitalize()
7376
raise AttributeError(f"{p_name} response missing usage info for {model}")
@@ -97,7 +100,8 @@ def ask_openai_compatible_structured[T](
97100
sys_msg: str | None = None,
98101
model: str = "gpt-4o",
99102
provider: str = "openai",
100-
record_store: "RecordStore | None" = None,
103+
record_store: RecordStore | None = None,
104+
temperature: float | None = None,
101105
) -> T:
102106
"""Execute structured call against an OpenAI-compatible API with retries."""
103107
max_attempts = 100
@@ -116,6 +120,7 @@ def ask_openai_compatible_structured[T](
116120
model=model,
117121
input=user_msg,
118122
instructions=sys_msg,
123+
temperature=temperature,
119124
)
120125
output = response.output_text
121126
else:
@@ -124,6 +129,7 @@ def ask_openai_compatible_structured[T](
124129
input=user_msg,
125130
text_format=response_type,
126131
instructions=sys_msg,
132+
temperature=temperature,
127133
)
128134
output = response.output_parsed
129135

@@ -170,7 +176,8 @@ def ask_openai[T](
170176
model: str = OpenAIModels.gpt5.value,
171177
*,
172178
client_override: OpenAI | None = None,
173-
record_store: "RecordStore | None" = None,
179+
record_store: RecordStore | None = None,
180+
temperature: float | None = None,
174181
) -> T:
175182
"""Call OpenAI API with automatic retry."""
176183
api_client = client_override or client
@@ -182,6 +189,7 @@ def ask_openai[T](
182189
model=model,
183190
provider="openai",
184191
record_store=record_store,
192+
temperature=temperature,
185193
)
186194

187195

covenance/clients/openrouter_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def ask_openrouter[T](
3939
*,
4040
client_override: OpenAI | None = None,
4141
record_store: "RecordStore | None" = None,
42+
temperature: float | None = None,
4243
) -> T:
4344
"""Call OpenRouter API with automatic retry."""
4445
api_client = client_override or client
@@ -50,6 +51,7 @@ def ask_openrouter[T](
5051
model=model,
5152
provider="openrouter",
5253
record_store=record_store,
54+
temperature=temperature,
5355
)
5456

5557

0 commit comments

Comments
 (0)