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.
@@ -0,0 +1,14 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ *.tsbuildinfo
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.egg-info/
8
+ .venv/
9
+ .env
10
+ .env.*
11
+ *.db
12
+ *.sqlite
13
+ .DS_Store
14
+ *.tgz
@@ -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,6 @@
1
+ """LangChain integration for the AI Stupid Level Smart Router."""
2
+
3
+ from langchain_aistupidlevel.chat_models import ChatAISL
4
+
5
+ __all__ = ["ChatAISL"]
6
+ __version__ = "0.1.0"
@@ -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
@@ -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)
@@ -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) == {}