langchain-aistupidlevel 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.
- langchain_aistupidlevel-0.1.0/.gitignore +14 -0
- langchain_aistupidlevel-0.1.0/LICENSE +21 -0
- langchain_aistupidlevel-0.1.0/PKG-INFO +86 -0
- langchain_aistupidlevel-0.1.0/README.md +60 -0
- langchain_aistupidlevel-0.1.0/langchain_aistupidlevel/__init__.py +6 -0
- langchain_aistupidlevel-0.1.0/langchain_aistupidlevel/chat_models.py +153 -0
- langchain_aistupidlevel-0.1.0/pyproject.toml +41 -0
- langchain_aistupidlevel-0.1.0/tests/__init__.py +0 -0
- langchain_aistupidlevel-0.1.0/tests/integration_tests/__init__.py +0 -0
- langchain_aistupidlevel-0.1.0/tests/integration_tests/conftest.py +13 -0
- langchain_aistupidlevel-0.1.0/tests/integration_tests/test_chat_models.py +48 -0
- langchain_aistupidlevel-0.1.0/tests/unit_tests/__init__.py +0 -0
- langchain_aistupidlevel-0.1.0/tests/unit_tests/test_chat_models.py +49 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Studio Platforms (AI Stupid Level)
|
|
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,86 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langchain-aistupidlevel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain chat model for the AI Stupid Level Smart Router: each request goes to the model that scores best for the task, with your own provider keys.
|
|
5
|
+
Project-URL: Homepage, https://aistupidlevel.info/router
|
|
6
|
+
Project-URL: Documentation, https://aistupidlevel.info/router/docs#integrations
|
|
7
|
+
Project-URL: Source, https://github.com/StudioPlatforms/aistupidlevel-sdk/tree/main/langchain-python
|
|
8
|
+
Project-URL: Issues, https://github.com/StudioPlatforms/aistupidlevel-sdk/issues
|
|
9
|
+
Author-email: AI Stupid Level <hello@aistupidlevel.info>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: aistupidlevel,langchain,llm,model-routing,openai-compatible,router
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: langchain-core<2,>=1.0
|
|
19
|
+
Requires-Dist: langchain-openai<2,>=1.0
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: langchain-tests<2,>=1.0; extra == 'test'
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
|
|
23
|
+
Requires-Dist: pytest-socket>=0.7; extra == 'test'
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# langchain-aistupidlevel
|
|
28
|
+
|
|
29
|
+
A LangChain chat model for the [AI Stupid Level](https://aistupidlevel.info/router) Smart Router.
|
|
30
|
+
|
|
31
|
+
Instead of a fixed model you pick a routing mode, such as `auto-coding`, `auto-reasoning`, `auto-tooling`, `auto-best` or `auto-value`. Each request goes to the model that scores best for that mode on AI Stupid Level's benchmarks, among the providers you connected (OpenAI, Anthropic, Google, DeepSeek, Kimi, GLM). The router calls that model with your own provider keys and adds nothing to token prices.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install -U langchain-aistupidlevel
|
|
37
|
+
export ASL_ROUTER_KEY="aism_..." # https://aistupidlevel.info/router/keys
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Use
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from langchain_aistupidlevel import ChatAISL
|
|
44
|
+
|
|
45
|
+
llm = ChatAISL(model="auto-coding")
|
|
46
|
+
msg = llm.invoke("Write a Python function that reverses a string.")
|
|
47
|
+
|
|
48
|
+
print(msg.content)
|
|
49
|
+
print(msg.response_metadata["routed_model"]) # the model that answered, e.g. claude-opus-5-5
|
|
50
|
+
print(msg.response_metadata["routed_provider"]) # e.g. anthropic
|
|
51
|
+
print(msg.response_metadata["routing_reason"]) # why the router chose it
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`ChatAISL` is built on `ChatOpenAI`, so tool calling (`bind_tools`), structured output (`with_structured_output`), streaming and async work the same way. A model id such as `claude-opus-5-5` instead of a routing mode pins that model.
|
|
55
|
+
|
|
56
|
+
**What it adds over `ChatOpenAI` with a base URL:** the router reports which model answered and why in response headers, which `ChatOpenAI` drops. `ChatAISL` copies them into `response_metadata` as `routed_model`, `routed_provider`, `routing_reason`, `routing_latency_ms` and `routing_request_id`.
|
|
57
|
+
|
|
58
|
+
### Sandbox
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
llm = ChatAISL(model="auto-tooling", sandbox=True)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The routing is real and the answer is a mock: nothing is sent to a model or counted. It is useful for building and testing before you add provider keys.
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
| Parameter | Environment variable | Default |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `api_key` | `ASL_ROUTER_KEY` | none |
|
|
71
|
+
| `base_url` | `ASL_BASE_URL` | `https://aistupidlevel.info/v1` |
|
|
72
|
+
| `model` | | `auto-best` |
|
|
73
|
+
| `sandbox` | | `False` |
|
|
74
|
+
|
|
75
|
+
The routing modes are described at https://aistupidlevel.info/router/docs#strategies. The router routes text and tool calls; image input is not supported.
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
pip install -e ".[test]"
|
|
81
|
+
pytest tests/unit_tests
|
|
82
|
+
ASL_ROUTER_KEY=aism_... pytest tests/integration_tests # sandbox: nothing spent
|
|
83
|
+
ASL_ROUTER_KEY=aism_... ASL_LIVE=1 pytest tests/integration_tests # real models, with provider keys
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
MIT licence.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# langchain-aistupidlevel
|
|
2
|
+
|
|
3
|
+
A LangChain chat model for the [AI Stupid Level](https://aistupidlevel.info/router) Smart Router.
|
|
4
|
+
|
|
5
|
+
Instead of a fixed model you pick a routing mode, such as `auto-coding`, `auto-reasoning`, `auto-tooling`, `auto-best` or `auto-value`. Each request goes to the model that scores best for that mode on AI Stupid Level's benchmarks, among the providers you connected (OpenAI, Anthropic, Google, DeepSeek, Kimi, GLM). The router calls that model with your own provider keys and adds nothing to token prices.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install -U langchain-aistupidlevel
|
|
11
|
+
export ASL_ROUTER_KEY="aism_..." # https://aistupidlevel.info/router/keys
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Use
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from langchain_aistupidlevel import ChatAISL
|
|
18
|
+
|
|
19
|
+
llm = ChatAISL(model="auto-coding")
|
|
20
|
+
msg = llm.invoke("Write a Python function that reverses a string.")
|
|
21
|
+
|
|
22
|
+
print(msg.content)
|
|
23
|
+
print(msg.response_metadata["routed_model"]) # the model that answered, e.g. claude-opus-5-5
|
|
24
|
+
print(msg.response_metadata["routed_provider"]) # e.g. anthropic
|
|
25
|
+
print(msg.response_metadata["routing_reason"]) # why the router chose it
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`ChatAISL` is built on `ChatOpenAI`, so tool calling (`bind_tools`), structured output (`with_structured_output`), streaming and async work the same way. A model id such as `claude-opus-5-5` instead of a routing mode pins that model.
|
|
29
|
+
|
|
30
|
+
**What it adds over `ChatOpenAI` with a base URL:** the router reports which model answered and why in response headers, which `ChatOpenAI` drops. `ChatAISL` copies them into `response_metadata` as `routed_model`, `routed_provider`, `routing_reason`, `routing_latency_ms` and `routing_request_id`.
|
|
31
|
+
|
|
32
|
+
### Sandbox
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
llm = ChatAISL(model="auto-tooling", sandbox=True)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The routing is real and the answer is a mock: nothing is sent to a model or counted. It is useful for building and testing before you add provider keys.
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
| Parameter | Environment variable | Default |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `api_key` | `ASL_ROUTER_KEY` | none |
|
|
45
|
+
| `base_url` | `ASL_BASE_URL` | `https://aistupidlevel.info/v1` |
|
|
46
|
+
| `model` | | `auto-best` |
|
|
47
|
+
| `sandbox` | | `False` |
|
|
48
|
+
|
|
49
|
+
The routing modes are described at https://aistupidlevel.info/router/docs#strategies. The router routes text and tool calls; image input is not supported.
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
pip install -e ".[test]"
|
|
55
|
+
pytest tests/unit_tests
|
|
56
|
+
ASL_ROUTER_KEY=aism_... pytest tests/integration_tests # sandbox: nothing spent
|
|
57
|
+
ASL_ROUTER_KEY=aism_... ASL_LIVE=1 pytest tests/integration_tests # real models, with provider keys
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
MIT licence.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""ChatAISL: the AI Stupid Level Smart Router as a LangChain chat model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, AsyncIterator, Iterator
|
|
7
|
+
|
|
8
|
+
from langchain_core.outputs import ChatGenerationChunk, ChatResult
|
|
9
|
+
from langchain_openai.chat_models.base import BaseChatOpenAI
|
|
10
|
+
from pydantic import Field, SecretStr, model_validator
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://aistupidlevel.info/v1"
|
|
13
|
+
|
|
14
|
+
# Response headers that say which model answered and why, copied into response_metadata.
|
|
15
|
+
_ROUTING_HEADERS = {
|
|
16
|
+
"x-aism-model": "routed_model",
|
|
17
|
+
"x-aism-provider": "routed_provider",
|
|
18
|
+
"x-aism-reasoning": "routing_reason",
|
|
19
|
+
"x-aism-latency": "routing_latency_ms",
|
|
20
|
+
"x-aism-request-id": "routing_request_id",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _routing_metadata(headers: dict | None) -> dict:
|
|
25
|
+
"""The routing facts from the router's response headers."""
|
|
26
|
+
if not headers:
|
|
27
|
+
return {}
|
|
28
|
+
lower = {str(k).lower(): v for k, v in headers.items()}
|
|
29
|
+
out = {name: lower[h] for h, name in _ROUTING_HEADERS.items() if lower.get(h)}
|
|
30
|
+
if "routing_latency_ms" in out:
|
|
31
|
+
try:
|
|
32
|
+
out["routing_latency_ms"] = int(out["routing_latency_ms"])
|
|
33
|
+
except ValueError:
|
|
34
|
+
out.pop("routing_latency_ms")
|
|
35
|
+
if lower.get("x-aism-sandbox") == "true":
|
|
36
|
+
out["sandbox"] = True
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ChatAISL(BaseChatOpenAI):
|
|
41
|
+
"""Chat model for the AI Stupid Level Smart Router.
|
|
42
|
+
|
|
43
|
+
Instead of a fixed model, pass a routing mode such as ``auto-coding``, ``auto-reasoning``,
|
|
44
|
+
``auto-tooling``, ``auto-best`` or ``auto-value``. Each request goes to the model that scores
|
|
45
|
+
best for that mode on AI Stupid Level's benchmarks, among the providers you connected, and is
|
|
46
|
+
called with your own provider keys. A model id such as ``claude-opus-5-5`` pins that model.
|
|
47
|
+
|
|
48
|
+
Setup:
|
|
49
|
+
.. code-block:: bash
|
|
50
|
+
|
|
51
|
+
pip install -U langchain-aistupidlevel
|
|
52
|
+
export ASL_ROUTER_KEY="aism_..." # https://aistupidlevel.info/router/keys
|
|
53
|
+
|
|
54
|
+
Instantiate:
|
|
55
|
+
.. code-block:: python
|
|
56
|
+
|
|
57
|
+
from langchain_aistupidlevel import ChatAISL
|
|
58
|
+
|
|
59
|
+
llm = ChatAISL(model="auto-coding")
|
|
60
|
+
|
|
61
|
+
Invoke:
|
|
62
|
+
.. code-block:: python
|
|
63
|
+
|
|
64
|
+
msg = llm.invoke("Write a Python function that reverses a string.")
|
|
65
|
+
msg.content
|
|
66
|
+
msg.response_metadata["routed_model"] # e.g. "claude-opus-5-5"
|
|
67
|
+
msg.response_metadata["routing_reason"] # why the router chose it
|
|
68
|
+
|
|
69
|
+
Sandbox (real routing, mock answers, nothing spent or counted):
|
|
70
|
+
.. code-block:: python
|
|
71
|
+
|
|
72
|
+
llm = ChatAISL(model="auto-coding", sandbox=True)
|
|
73
|
+
|
|
74
|
+
Tool calling, structured output and streaming work as with ``ChatOpenAI``.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
model_name: str = Field(default="auto-best", alias="model")
|
|
78
|
+
"""A routing mode (``auto-coding``, ``auto-best``, …) or a model id to pin."""
|
|
79
|
+
|
|
80
|
+
openai_api_key: SecretStr | None = Field(
|
|
81
|
+
alias="api_key",
|
|
82
|
+
default_factory=lambda: SecretStr(os.environ["ASL_ROUTER_KEY"]) if os.environ.get("ASL_ROUTER_KEY") else None,
|
|
83
|
+
)
|
|
84
|
+
"""Router key (``aism_…``). Read from ``ASL_ROUTER_KEY`` when not given."""
|
|
85
|
+
|
|
86
|
+
openai_api_base: str | None = Field(
|
|
87
|
+
alias="base_url",
|
|
88
|
+
default_factory=lambda: os.environ.get("ASL_BASE_URL") or DEFAULT_BASE_URL,
|
|
89
|
+
)
|
|
90
|
+
"""Router endpoint. Read from ``ASL_BASE_URL`` when set."""
|
|
91
|
+
|
|
92
|
+
sandbox: bool = False
|
|
93
|
+
"""Real routing with a mock answer: nothing is sent to a model or counted."""
|
|
94
|
+
|
|
95
|
+
stream_usage: bool | None = True
|
|
96
|
+
"""Ask for token usage at the end of a stream, as ChatOpenAI does for OpenAI."""
|
|
97
|
+
|
|
98
|
+
include_response_headers: bool = True
|
|
99
|
+
"""Kept on so the routing headers can be copied into ``response_metadata``."""
|
|
100
|
+
|
|
101
|
+
@model_validator(mode="before")
|
|
102
|
+
@classmethod
|
|
103
|
+
def _sandbox_header(cls, values: Any) -> Any:
|
|
104
|
+
if isinstance(values, dict) and values.get("sandbox"):
|
|
105
|
+
headers = dict(values.get("default_headers") or {})
|
|
106
|
+
headers["X-ASL-Sandbox"] = "true"
|
|
107
|
+
values["default_headers"] = headers
|
|
108
|
+
return values
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def _llm_type(self) -> str:
|
|
112
|
+
return "aistupidlevel-chat"
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def lc_secrets(self) -> dict[str, str]:
|
|
116
|
+
return {"openai_api_key": "ASL_ROUTER_KEY"}
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_lc_namespace(cls) -> list[str]:
|
|
120
|
+
return ["langchain_aistupidlevel", "chat_models"]
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def is_lc_serializable(cls) -> bool:
|
|
124
|
+
# langchain-core only revives classes it lists in its own mapping, so a
|
|
125
|
+
# serialized ChatAISL could not be loaded again.
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def _get_ls_params(self, stop: list[str] | None = None, **kwargs: Any) -> Any:
|
|
129
|
+
params = super()._get_ls_params(stop=stop, **kwargs)
|
|
130
|
+
params["ls_provider"] = "aistupidlevel"
|
|
131
|
+
return params
|
|
132
|
+
|
|
133
|
+
def _create_chat_result(self, response: Any, generation_info: dict | None = None) -> ChatResult:
|
|
134
|
+
result = super()._create_chat_result(response, generation_info)
|
|
135
|
+
routing = _routing_metadata((generation_info or {}).get("headers"))
|
|
136
|
+
if routing:
|
|
137
|
+
for gen in result.generations:
|
|
138
|
+
gen.message.response_metadata.update(routing)
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
def _stream(self, *args: Any, **kwargs: Any) -> Iterator[ChatGenerationChunk]:
|
|
142
|
+
for chunk in super()._stream(*args, **kwargs):
|
|
143
|
+
routing = _routing_metadata((chunk.generation_info or {}).get("headers"))
|
|
144
|
+
if routing:
|
|
145
|
+
chunk.message.response_metadata.update(routing)
|
|
146
|
+
yield chunk
|
|
147
|
+
|
|
148
|
+
async def _astream(self, *args: Any, **kwargs: Any) -> AsyncIterator[ChatGenerationChunk]:
|
|
149
|
+
async for chunk in super()._astream(*args, **kwargs):
|
|
150
|
+
routing = _routing_metadata((chunk.generation_info or {}).get("headers"))
|
|
151
|
+
if routing:
|
|
152
|
+
chunk.message.response_metadata.update(routing)
|
|
153
|
+
yield chunk
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-aistupidlevel"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LangChain chat model for the AI Stupid Level Smart Router: each request goes to the model that scores best for the task, with your own provider keys."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "AI Stupid Level", email = "hello@aistupidlevel.info" }]
|
|
14
|
+
keywords = ["langchain", "llm", "router", "aistupidlevel", "openai-compatible", "model-routing"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["langchain-openai>=1.0,<2", "langchain-core>=1.0,<2"]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
test = ["pytest>=8", "pytest-asyncio>=0.23", "pytest-socket>=0.7", "langchain-tests>=1.0,<2"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://aistupidlevel.info/router"
|
|
28
|
+
Documentation = "https://aistupidlevel.info/router/docs#integrations"
|
|
29
|
+
Source = "https://github.com/StudioPlatforms/aistupidlevel-sdk/tree/main/langchain-python"
|
|
30
|
+
Issues = "https://github.com/StudioPlatforms/aistupidlevel-sdk/issues"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["langchain_aistupidlevel"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.sdist]
|
|
36
|
+
include = ["langchain_aistupidlevel", "tests", "README.md", "LICENSE", "pyproject.toml"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
asyncio_mode = "auto"
|
|
40
|
+
asyncio_default_fixture_loop_scope = "session"
|
|
41
|
+
asyncio_default_test_loop_scope = "session"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
SANDBOX_ONLY_FAILS = ("test_tool_calling", "test_tool_calling_async", "test_unicode_tool_call_integration")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def pytest_collection_modifyitems(items):
|
|
9
|
+
if os.environ.get("ASL_LIVE") == "1":
|
|
10
|
+
return
|
|
11
|
+
for item in items:
|
|
12
|
+
if item.originalname in SANDBOX_ONLY_FAILS:
|
|
13
|
+
item.add_marker(pytest.mark.xfail(reason="sandbox answers use placeholder tool arguments", strict=True))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Standard LangChain integration tests. Needs ASL_ROUTER_KEY.
|
|
2
|
+
|
|
3
|
+
By default they run in sandbox mode: real routing, mock answers, nothing spent. A mock answer
|
|
4
|
+
fills tool arguments with placeholders, so the tests that check the arguments' values are
|
|
5
|
+
expected to fail there. Set ASL_LIVE=1 (with provider keys on the account) to run every test
|
|
6
|
+
against real models."""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from langchain_tests.integration_tests import ChatModelIntegrationTests
|
|
12
|
+
|
|
13
|
+
from langchain_aistupidlevel import ChatAISL
|
|
14
|
+
|
|
15
|
+
pytestmark = pytest.mark.skipif(not os.environ.get("ASL_ROUTER_KEY"), reason="ASL_ROUTER_KEY not set")
|
|
16
|
+
|
|
17
|
+
LIVE = os.environ.get("ASL_LIVE") == "1"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestChatAISLIntegration(ChatModelIntegrationTests):
|
|
21
|
+
@property
|
|
22
|
+
def chat_model_class(self) -> type[ChatAISL]:
|
|
23
|
+
return ChatAISL
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def chat_model_params(self) -> dict:
|
|
27
|
+
return {"model": "auto-tooling", "sandbox": not LIVE, "temperature": 0}
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def supports_image_inputs(self) -> bool:
|
|
31
|
+
return False # the router routes text and tool calls only
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def returns_usage_metadata(self) -> bool:
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_routing_metadata_on_invoke():
|
|
39
|
+
msg = ChatAISL(model="auto-coding", sandbox=True).invoke("Write a Python function that reverses a string.")
|
|
40
|
+
assert msg.response_metadata["routed_model"]
|
|
41
|
+
assert msg.response_metadata["routed_provider"]
|
|
42
|
+
assert msg.response_metadata["routing_reason"].startswith("Sandbox")
|
|
43
|
+
assert msg.response_metadata["sandbox"] is True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_routing_metadata_on_stream():
|
|
47
|
+
chunks = list(ChatAISL(model="auto-coding", sandbox=True).stream("Say hi."))
|
|
48
|
+
assert any(c.response_metadata.get("routed_model") for c in chunks)
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Standard LangChain unit tests plus the routing metadata."""
|
|
2
|
+
|
|
3
|
+
from langchain_tests.unit_tests import ChatModelUnitTests
|
|
4
|
+
|
|
5
|
+
from langchain_aistupidlevel import ChatAISL
|
|
6
|
+
from langchain_aistupidlevel.chat_models import _routing_metadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestChatAISLUnit(ChatModelUnitTests):
|
|
10
|
+
@property
|
|
11
|
+
def chat_model_class(self) -> type[ChatAISL]:
|
|
12
|
+
return ChatAISL
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def chat_model_params(self) -> dict:
|
|
16
|
+
return {"model": "auto-coding", "api_key": "aism_test"}
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def init_from_env_params(self) -> tuple[dict, dict, dict]:
|
|
20
|
+
return (
|
|
21
|
+
{"ASL_ROUTER_KEY": "aism_env", "ASL_BASE_URL": "https://example.test/v1"},
|
|
22
|
+
{"model": "auto-best"},
|
|
23
|
+
{"openai_api_key": "aism_env", "openai_api_base": "https://example.test/v1"},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_defaults_point_at_the_router(monkeypatch):
|
|
28
|
+
monkeypatch.delenv("ASL_BASE_URL", raising=False)
|
|
29
|
+
llm = ChatAISL(api_key="aism_x")
|
|
30
|
+
assert llm.openai_api_base == "https://aistupidlevel.info/v1"
|
|
31
|
+
assert llm.model_name == "auto-best"
|
|
32
|
+
assert llm.include_response_headers is True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_sandbox_sets_the_header():
|
|
36
|
+
llm = ChatAISL(api_key="aism_x", sandbox=True, default_headers={"X-Other": "1"})
|
|
37
|
+
assert llm.default_headers == {"X-Other": "1", "X-ASL-Sandbox": "true"}
|
|
38
|
+
assert ChatAISL(api_key="aism_x").default_headers is None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_routing_metadata_from_headers():
|
|
42
|
+
meta = _routing_metadata({
|
|
43
|
+
"X-AISM-Model": "claude-opus-5-5", "x-aism-provider": "anthropic",
|
|
44
|
+
"x-aism-reasoning": "Ranked #1", "x-aism-latency": "1420", "x-aism-sandbox": "true",
|
|
45
|
+
"content-type": "application/json",
|
|
46
|
+
})
|
|
47
|
+
assert meta == {"routed_model": "claude-opus-5-5", "routed_provider": "anthropic",
|
|
48
|
+
"routing_reason": "Ranked #1", "routing_latency_ms": 1420, "sandbox": True}
|
|
49
|
+
assert _routing_metadata(None) == {}
|