3tears-models 0.14.0__py3-none-any.whl
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.
- 3tears_models-0.14.0.dist-info/METADATA +62 -0
- 3tears_models-0.14.0.dist-info/RECORD +35 -0
- 3tears_models-0.14.0.dist-info/WHEEL +4 -0
- 3tears_models-0.14.0.dist-info/licenses/LICENSE +21 -0
- threetears/models/__init__.py +175 -0
- threetears/models/cache.py +99 -0
- threetears/models/capabilities.py +406 -0
- threetears/models/chunk_merging.py +70 -0
- threetears/models/chunk_parsing.py +120 -0
- threetears/models/circuit_breaker.py +378 -0
- threetears/models/defaults.py +68 -0
- threetears/models/enums.py +56 -0
- threetears/models/errors.py +138 -0
- threetears/models/factory.py +320 -0
- threetears/models/preprocessing.py +223 -0
- threetears/models/price_lookup.py +614 -0
- threetears/models/providers/__init__.py +8 -0
- threetears/models/providers/_claude_cli.py +98 -0
- threetears/models/providers/_voyageai_compat.py +86 -0
- threetears/models/providers/anthropic.py +549 -0
- threetears/models/providers/image/__init__.py +22 -0
- threetears/models/providers/image/a1111.py +119 -0
- threetears/models/providers/image/comfyui.py +218 -0
- threetears/models/providers/image/huggingface.py +94 -0
- threetears/models/providers/image/modelslab.py +150 -0
- threetears/models/providers/image/openai.py +189 -0
- threetears/models/providers/openai.py +339 -0
- threetears/models/providers/openrouter.py +550 -0
- threetears/models/providers/voyageai.py +109 -0
- threetears/models/providers/whisper.py +214 -0
- threetears/models/registry.py +116 -0
- threetears/models/registry_loader.py +210 -0
- threetears/models/tool_name_translation.py +311 -0
- threetears/models/tool_name_validation.py +186 -0
- threetears/models/tracking.py +896 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 3tears-models
|
|
3
|
+
Version: 0.14.0
|
|
4
|
+
Summary: LangChain-native AI model factories with capability metadata, circuit breakers, and usage tracking for the 3tears platform
|
|
5
|
+
Project-URL: Repository, https://github.com/pacepace/3tears
|
|
6
|
+
Author: pace
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.14
|
|
10
|
+
Requires-Dist: 3tears-media-contracts
|
|
11
|
+
Requires-Dist: 3tears-observe
|
|
12
|
+
Requires-Dist: langchain-anthropic>=1.4
|
|
13
|
+
Requires-Dist: langchain-core>=0.3
|
|
14
|
+
Requires-Dist: langchain-openai>=1.1
|
|
15
|
+
Requires-Dist: langchain-openrouter>=0.1
|
|
16
|
+
Requires-Dist: pydantic>=2.0
|
|
17
|
+
Provides-Extra: claude-cli
|
|
18
|
+
Requires-Dist: langchain-claude-code>=0.1.0; extra == 'claude-cli'
|
|
19
|
+
Provides-Extra: image
|
|
20
|
+
Requires-Dist: httpx>=0.27; extra == 'image'
|
|
21
|
+
Provides-Extra: prometheus
|
|
22
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'prometheus'
|
|
23
|
+
Provides-Extra: voyageai
|
|
24
|
+
Requires-Dist: langchain-voyageai>=0.3; extra == 'voyageai'
|
|
25
|
+
Provides-Extra: whisper
|
|
26
|
+
Requires-Dist: httpx>=0.27; extra == 'whisper'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# 3tears-models
|
|
30
|
+
|
|
31
|
+
LangChain-native AI model factories for the 3tears framework. Build chat and embedding models from a single call, with capability metadata, circuit breakers, error translation, and usage tracking wired in.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install 3tears-models
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## What you get
|
|
38
|
+
|
|
39
|
+
- **Factories** -- `create_chat_model()` and `create_embedding_model()` return standard LangChain `BaseChatModel` / `Embeddings` instances. No custom runtime protocols to learn.
|
|
40
|
+
- **Providers** -- Anthropic, OpenAI, OpenRouter, VoyageAI, Whisper, and image backends (OpenAI Images, HuggingFace, A1111, ModelsLab, ComfyUI).
|
|
41
|
+
- **Capability registry** -- `get_capabilities()`, `register_capabilities()`, and per-model overrides describe context windows, vision support, tool support, and tier.
|
|
42
|
+
- **Circuit breakers** -- `CircuitBreaker` and `CircuitBreakerRegistry` trip on repeated provider failures and recover on a timer.
|
|
43
|
+
- **Usage tracking** -- `UsageTracker.record()` emits an OpenTelemetry span plus Prometheus instruments, with optional per-application audit and counter sinks.
|
|
44
|
+
- **Message hygiene** -- `preprocess_messages()`, `enforce_alternating_roles()`, `filter_invalid_tool_calls()`, and streaming chunk helpers (`parse_chunk`, `merge_chunks`).
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from threetears.models import create_chat_model, create_embedding_model
|
|
50
|
+
|
|
51
|
+
chat = create_chat_model("claude-sonnet-4-6")
|
|
52
|
+
reply = await chat.ainvoke("Summarize three-tier caching in one sentence.")
|
|
53
|
+
|
|
54
|
+
embedder = create_embedding_model("voyage-3")
|
|
55
|
+
vectors = await embedder.aembed_documents(["first doc", "second doc"])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Capabilities, circuit breakers, and usage tracking attach automatically. Reach for the registry and tracker APIs directly when you need to inspect or override them.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
threetears/models/__init__.py,sha256=cQyOaVkoaBJXPo_IL_sCeiNB4cFvHinEzah-SesDyHE,5657
|
|
2
|
+
threetears/models/cache.py,sha256=DW4AJd1UevXfz1PdH8EMNTzBatQDWyhZw9m3RTLdC-I,3107
|
|
3
|
+
threetears/models/capabilities.py,sha256=J3oRJk0Nb5h6B_JaxVdoqBiGKosnAllxi17zm6KPHEI,18304
|
|
4
|
+
threetears/models/chunk_merging.py,sha256=xYAY3pjYzoiJJL_LXM-vf4q_fQ9v70vc533DjyCZvA8,3212
|
|
5
|
+
threetears/models/chunk_parsing.py,sha256=0jsb0N0wNTSeOpxONqEiKHPX8MZEPiZFCehMacVvRMg,4940
|
|
6
|
+
threetears/models/circuit_breaker.py,sha256=bsFOABv-NcMWgcRLEfalNZ-uBGF-XLwostE_I0HgqfU,12745
|
|
7
|
+
threetears/models/defaults.py,sha256=XjSNKGCes1pWcnv-QxENrG-h2V23KfnKzLypumszFcE,2985
|
|
8
|
+
threetears/models/enums.py,sha256=bMYt-u4TcEuxU8g5d_t-R7alJ8OV7oifI6e_9_rQSQ0,1414
|
|
9
|
+
threetears/models/errors.py,sha256=zbMfwLHQRqQ7ItXErAKApj8ml7q4CbNdUy7Nry8jyss,5695
|
|
10
|
+
threetears/models/factory.py,sha256=rD-63YsFmzrBVWh29aRUtzmUPhrIWMK0k4uPlFg4uEo,11709
|
|
11
|
+
threetears/models/preprocessing.py,sha256=IWt2mK6ekja8ByjO9SEDDnjf7BqUFC8_ZXyhEEIZRHc,6784
|
|
12
|
+
threetears/models/price_lookup.py,sha256=816Vr6HCz4eKHytOFhcpD-CxbSXLCBXGA_I5w1snDOM,26326
|
|
13
|
+
threetears/models/registry.py,sha256=YqLHaXKllPySTdHNP84BMpXaY7u3QuO1DsdiFLnyacs,3951
|
|
14
|
+
threetears/models/registry_loader.py,sha256=9tGewFaiWmElOPhOb6sb0xFCVODeBuIVaSuq4AJGAzw,8057
|
|
15
|
+
threetears/models/tool_name_translation.py,sha256=JQhRmHUQBo0lEDikomYyYbaXXXAaO5hDtGWypnf1tZc,12800
|
|
16
|
+
threetears/models/tool_name_validation.py,sha256=5ioli_7YiNO_uHyi-1Gqket3DP1EyS6uXvj4DwnELeA,8235
|
|
17
|
+
threetears/models/tracking.py,sha256=oz03j3f2uvXhjQDsRwBRcWHxCA7f7Bj6SGjaqjj6Fdg,36681
|
|
18
|
+
threetears/models/providers/__init__.py,sha256=NPwLiadG-79CDvCgQrRc7FcqAlnDTR3yw6lc5bEEGS0,361
|
|
19
|
+
threetears/models/providers/_claude_cli.py,sha256=GlSl19krhGoOG8wKhh9S5RH4eZnz0oWsnzl2Tn3lKOg,5158
|
|
20
|
+
threetears/models/providers/_voyageai_compat.py,sha256=POymh980gmoAlfzBys_QYo71x6H8n_ultkqGSOvpWt4,2936
|
|
21
|
+
threetears/models/providers/anthropic.py,sha256=1d1GPbVNfg16XOXdKaFpCduBpsC5rZ1vVd0ivj-Ebys,23307
|
|
22
|
+
threetears/models/providers/openai.py,sha256=uNYkzOj5S7kI-Hdz6a7bM3a66lWtkg2-NYQp52ANQXc,13000
|
|
23
|
+
threetears/models/providers/openrouter.py,sha256=S8_m86yhKpMeDut8VSlNsENceulAAWxdKfa_6mFWJw4,25158
|
|
24
|
+
threetears/models/providers/voyageai.py,sha256=ISTS2S9EFsl9eoJlOs2k5aDm4LD1iAqBr_Sb306dMQM,3783
|
|
25
|
+
threetears/models/providers/whisper.py,sha256=-SAZ7xsV2n573MhdSR_QARwJIxDilyFC9HsKuzme9JY,6428
|
|
26
|
+
threetears/models/providers/image/__init__.py,sha256=KD63m-c8RYPcM9izGx8EpkGgQ1gkeE2r3TEBC4AW6gY,824
|
|
27
|
+
threetears/models/providers/image/a1111.py,sha256=VnbsY8KhDVqBDpdtEQ16nLVN4-I9ZmBxp38pTm7KklI,3813
|
|
28
|
+
threetears/models/providers/image/comfyui.py,sha256=Poh-3rRYAysA_Mxv0qtyFZxk0VYJFI6jCA2SY4ImM9w,6645
|
|
29
|
+
threetears/models/providers/image/huggingface.py,sha256=yaLAL2Hnw9jt_m3TtnOwYTcyg4dNZl29VKQkixAwYN8,3069
|
|
30
|
+
threetears/models/providers/image/modelslab.py,sha256=7GZm1DYrPlsILD3MB9zCuCLUgDZYiTzEftCBDVseX7k,4866
|
|
31
|
+
threetears/models/providers/image/openai.py,sha256=8hU4gjcC9_aaCO6y-V6Kgw5S4MbtBpwFX71517CiKvA,5847
|
|
32
|
+
3tears_models-0.14.0.dist-info/METADATA,sha256=ckSKxqk5tO7k4pTCCXfZ3_RIpLx6IIcnIIHDcBWUj2o,2816
|
|
33
|
+
3tears_models-0.14.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
34
|
+
3tears_models-0.14.0.dist-info/licenses/LICENSE,sha256=7GWEoEOcFJenZLt4LDzqH2K7QLxo_2m8rzG7Vv8VGXo,1066
|
|
35
|
+
3tears_models-0.14.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Pace
|
|
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,175 @@
|
|
|
1
|
+
"""LangChain-native AI model factories with capability metadata, circuit
|
|
2
|
+
breakers, and usage tracking.
|
|
3
|
+
|
|
4
|
+
3tears v0.6.0+ exposes provider factory functions that return configured
|
|
5
|
+
LangChain ``BaseChatModel`` and ``Embeddings`` instances. The legacy
|
|
6
|
+
``ChatProvider`` / ``EmbeddingProvider`` / ``TranscriptionProvider`` /
|
|
7
|
+
``SpeechProvider`` / ``RerankingProvider`` runtime protocols have been
|
|
8
|
+
removed.
|
|
9
|
+
|
|
10
|
+
Importing this package eagerly loads the builtin provider modules so
|
|
11
|
+
their import-time :func:`register_capabilities` calls populate the
|
|
12
|
+
shared registry. This makes :func:`create_chat_model` /
|
|
13
|
+
:func:`create_embedding_model` work for every builtin model id without
|
|
14
|
+
the caller manually importing ``threetears.models.providers.<name>``.
|
|
15
|
+
The provider modules themselves keep ``langchain_<provider>`` imports
|
|
16
|
+
inside ``TYPE_CHECKING`` and inside their factory bodies, so the
|
|
17
|
+
eager-load only pulls capability metadata — actually instantiating a
|
|
18
|
+
provider model still imports its langchain backend lazily, preserving
|
|
19
|
+
the "install only the providers you use" property for production
|
|
20
|
+
callers.
|
|
21
|
+
|
|
22
|
+
v0.9.0 adds chat-model utility modules used across every consumer:
|
|
23
|
+
:mod:`threetears.models.chunk_merging` (merge streamed AIMessageChunks
|
|
24
|
+
into a single AIMessage), :mod:`threetears.models.chunk_parsing`
|
|
25
|
+
(extract text + reasoning per chunk), and
|
|
26
|
+
:mod:`threetears.models.tool_name_validation` (defend against
|
|
27
|
+
malformed tool names emitted by misbehaving providers). The
|
|
28
|
+
OpenRouter and Anthropic wrappers invoke the validator on every
|
|
29
|
+
response so junk tool names never reach downstream dispatch.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from threetears.models.cache import ModelCache
|
|
35
|
+
from threetears.models.capabilities import (
|
|
36
|
+
CapabilityOverride,
|
|
37
|
+
ModelCapabilities,
|
|
38
|
+
clear_capability_overrides,
|
|
39
|
+
get_capabilities,
|
|
40
|
+
get_capability_override,
|
|
41
|
+
list_capabilities,
|
|
42
|
+
register_capabilities,
|
|
43
|
+
register_capability_override,
|
|
44
|
+
unregister_capability_override,
|
|
45
|
+
)
|
|
46
|
+
from threetears.models.defaults import (
|
|
47
|
+
CURRENT_ANTHROPIC_CHAT_MODELS,
|
|
48
|
+
CURRENT_VOYAGEAI_EMBEDDING_MODELS,
|
|
49
|
+
DEFAULT_CHAT_MODEL,
|
|
50
|
+
DEFAULT_EMBEDDING_MODEL,
|
|
51
|
+
DEFAULT_FAST_MODEL,
|
|
52
|
+
DEFAULT_LARGE_MODEL,
|
|
53
|
+
)
|
|
54
|
+
from threetears.models.chunk_merging import merge_chunks
|
|
55
|
+
from threetears.models.chunk_parsing import ChunkParsed, parse_chunk
|
|
56
|
+
from threetears.models.circuit_breaker import (
|
|
57
|
+
CircuitBreaker,
|
|
58
|
+
CircuitBreakerCallback,
|
|
59
|
+
CircuitBreakerRegistry,
|
|
60
|
+
CircuitOpenError,
|
|
61
|
+
CircuitState,
|
|
62
|
+
)
|
|
63
|
+
from threetears.models.enums import ModelStatus, ModelTier, ModelType
|
|
64
|
+
from threetears.models.errors import friendly_api_error, identify_provider
|
|
65
|
+
from threetears.models.factory import create_chat_model, create_embedding_model
|
|
66
|
+
from threetears.models.preprocessing import (
|
|
67
|
+
enforce_alternating_roles,
|
|
68
|
+
format_vision_content,
|
|
69
|
+
preprocess_messages,
|
|
70
|
+
)
|
|
71
|
+
from threetears.models.price_lookup import (
|
|
72
|
+
OPENROUTER_MODELS_URL,
|
|
73
|
+
VOYAGE_PRICING_URL,
|
|
74
|
+
ModelOption,
|
|
75
|
+
OpenRouterPriceSource,
|
|
76
|
+
PriceLookup,
|
|
77
|
+
PriceSource,
|
|
78
|
+
VoyagePriceSource,
|
|
79
|
+
default_price_sources,
|
|
80
|
+
fetch_provider_models,
|
|
81
|
+
list_provider_models,
|
|
82
|
+
lookup_price,
|
|
83
|
+
match_price,
|
|
84
|
+
register_price_source,
|
|
85
|
+
)
|
|
86
|
+
from threetears.models.registry import BUILTIN_PROVIDERS, ProviderRegistry
|
|
87
|
+
from threetears.models.tool_name_validation import (
|
|
88
|
+
ToolNameValidationError,
|
|
89
|
+
filter_invalid_tool_calls,
|
|
90
|
+
is_valid_tool_name,
|
|
91
|
+
validate_tool_name,
|
|
92
|
+
)
|
|
93
|
+
from threetears.models.tracking import (
|
|
94
|
+
LlmPurpose,
|
|
95
|
+
UsageAuditSink,
|
|
96
|
+
UsageCounterSink,
|
|
97
|
+
UsageRecord,
|
|
98
|
+
UsageTracker,
|
|
99
|
+
UsageTrackingCallback,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Eager-import builtin provider modules so their import-time
|
|
103
|
+
# `register_capabilities()` calls populate the shared registry. The
|
|
104
|
+
# provider modules themselves do not import their respective
|
|
105
|
+
# `langchain_<provider>` package at module scope (those imports live
|
|
106
|
+
# inside TYPE_CHECKING + factory bodies) so this is metadata-only.
|
|
107
|
+
from threetears.models.providers import ( # noqa: E402, F401
|
|
108
|
+
anthropic as _anthropic_caps,
|
|
109
|
+
openai as _openai_caps,
|
|
110
|
+
openrouter as _openrouter_caps,
|
|
111
|
+
voyageai as _voyageai_caps,
|
|
112
|
+
whisper as _whisper_caps,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
__all__ = [
|
|
116
|
+
"BUILTIN_PROVIDERS",
|
|
117
|
+
"CURRENT_ANTHROPIC_CHAT_MODELS",
|
|
118
|
+
"CURRENT_VOYAGEAI_EMBEDDING_MODELS",
|
|
119
|
+
"DEFAULT_CHAT_MODEL",
|
|
120
|
+
"DEFAULT_EMBEDDING_MODEL",
|
|
121
|
+
"DEFAULT_FAST_MODEL",
|
|
122
|
+
"DEFAULT_LARGE_MODEL",
|
|
123
|
+
"CapabilityOverride",
|
|
124
|
+
"ChunkParsed",
|
|
125
|
+
"CircuitBreaker",
|
|
126
|
+
"CircuitBreakerCallback",
|
|
127
|
+
"CircuitBreakerRegistry",
|
|
128
|
+
"CircuitOpenError",
|
|
129
|
+
"CircuitState",
|
|
130
|
+
"LlmPurpose",
|
|
131
|
+
"ModelCache",
|
|
132
|
+
"ModelCapabilities",
|
|
133
|
+
"ModelOption",
|
|
134
|
+
"ModelStatus",
|
|
135
|
+
"ModelTier",
|
|
136
|
+
"ModelType",
|
|
137
|
+
"OPENROUTER_MODELS_URL",
|
|
138
|
+
"OpenRouterPriceSource",
|
|
139
|
+
"PriceLookup",
|
|
140
|
+
"PriceSource",
|
|
141
|
+
"ProviderRegistry",
|
|
142
|
+
"VOYAGE_PRICING_URL",
|
|
143
|
+
"VoyagePriceSource",
|
|
144
|
+
"ToolNameValidationError",
|
|
145
|
+
"UsageAuditSink",
|
|
146
|
+
"UsageCounterSink",
|
|
147
|
+
"UsageRecord",
|
|
148
|
+
"UsageTracker",
|
|
149
|
+
"UsageTrackingCallback",
|
|
150
|
+
"clear_capability_overrides",
|
|
151
|
+
"create_chat_model",
|
|
152
|
+
"create_embedding_model",
|
|
153
|
+
"default_price_sources",
|
|
154
|
+
"enforce_alternating_roles",
|
|
155
|
+
"fetch_provider_models",
|
|
156
|
+
"filter_invalid_tool_calls",
|
|
157
|
+
"format_vision_content",
|
|
158
|
+
"friendly_api_error",
|
|
159
|
+
"get_capabilities",
|
|
160
|
+
"get_capability_override",
|
|
161
|
+
"identify_provider",
|
|
162
|
+
"is_valid_tool_name",
|
|
163
|
+
"list_capabilities",
|
|
164
|
+
"list_provider_models",
|
|
165
|
+
"lookup_price",
|
|
166
|
+
"match_price",
|
|
167
|
+
"merge_chunks",
|
|
168
|
+
"parse_chunk",
|
|
169
|
+
"preprocess_messages",
|
|
170
|
+
"register_capabilities",
|
|
171
|
+
"register_capability_override",
|
|
172
|
+
"register_price_source",
|
|
173
|
+
"unregister_capability_override",
|
|
174
|
+
"validate_tool_name",
|
|
175
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""thread-safe instance cache for model provider objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ModelCache",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModelCache:
|
|
14
|
+
"""thread-safe cache for model provider instances.
|
|
15
|
+
|
|
16
|
+
stores instantiated model objects keyed by (provider_name, model_name)
|
|
17
|
+
tuple to avoid re-instantiation on every request. supports selective
|
|
18
|
+
eviction for API key rotation and provider shutdown scenarios.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._cache: dict[tuple[str, str], Any] = {}
|
|
23
|
+
self._lock: threading.Lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
def get(self, provider_name: str, model_name: str) -> Any | None:
|
|
26
|
+
"""retrieve cached model instance.
|
|
27
|
+
|
|
28
|
+
:param provider_name: provider identifier (e.g. "anthropic")
|
|
29
|
+
:ptype provider_name: str
|
|
30
|
+
:param model_name: model identifier (e.g. "claude-sonnet-4-6")
|
|
31
|
+
:ptype model_name: str
|
|
32
|
+
:return: cached instance or None if not found
|
|
33
|
+
:rtype: Any | None
|
|
34
|
+
"""
|
|
35
|
+
with self._lock:
|
|
36
|
+
result = self._cache.get((provider_name, model_name))
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
def put(self, provider_name: str, model_name: str, instance: Any) -> None:
|
|
40
|
+
"""store model instance in cache.
|
|
41
|
+
|
|
42
|
+
:param provider_name: provider identifier
|
|
43
|
+
:ptype provider_name: str
|
|
44
|
+
:param model_name: model identifier
|
|
45
|
+
:ptype model_name: str
|
|
46
|
+
:param instance: model instance to cache
|
|
47
|
+
:ptype instance: Any
|
|
48
|
+
"""
|
|
49
|
+
with self._lock:
|
|
50
|
+
self._cache[(provider_name, model_name)] = instance
|
|
51
|
+
|
|
52
|
+
def evict(self, provider_name: str, model_name: str) -> bool:
|
|
53
|
+
"""remove specific model instance from cache.
|
|
54
|
+
|
|
55
|
+
:param provider_name: provider identifier
|
|
56
|
+
:ptype provider_name: str
|
|
57
|
+
:param model_name: model identifier
|
|
58
|
+
:ptype model_name: str
|
|
59
|
+
:return: True if entry was found and removed, False if not found
|
|
60
|
+
:rtype: bool
|
|
61
|
+
"""
|
|
62
|
+
with self._lock:
|
|
63
|
+
key = (provider_name, model_name)
|
|
64
|
+
if key in self._cache:
|
|
65
|
+
del self._cache[key]
|
|
66
|
+
found = True
|
|
67
|
+
else:
|
|
68
|
+
found = False
|
|
69
|
+
return found
|
|
70
|
+
|
|
71
|
+
def evict_provider(self, provider_name: str) -> int:
|
|
72
|
+
"""remove all cached instances for given provider.
|
|
73
|
+
|
|
74
|
+
:param provider_name: provider identifier
|
|
75
|
+
:ptype provider_name: str
|
|
76
|
+
:return: number of entries removed
|
|
77
|
+
:rtype: int
|
|
78
|
+
"""
|
|
79
|
+
with self._lock:
|
|
80
|
+
keys_to_remove = [key for key in self._cache if key[0] == provider_name]
|
|
81
|
+
for key in keys_to_remove:
|
|
82
|
+
del self._cache[key]
|
|
83
|
+
count = len(keys_to_remove)
|
|
84
|
+
return count
|
|
85
|
+
|
|
86
|
+
def clear(self) -> None:
|
|
87
|
+
"""remove all entries from cache."""
|
|
88
|
+
with self._lock:
|
|
89
|
+
self._cache.clear()
|
|
90
|
+
|
|
91
|
+
def size(self) -> int:
|
|
92
|
+
"""return number of cached entries.
|
|
93
|
+
|
|
94
|
+
:return: current cache size
|
|
95
|
+
:rtype: int
|
|
96
|
+
"""
|
|
97
|
+
with self._lock:
|
|
98
|
+
result = len(self._cache)
|
|
99
|
+
return result
|