modelos-secure 0.0.0-stage → 0.1.0
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.
- package/README.md +38 -2
- package/assets/login-B21lxf7i.js +553 -0
- package/assets/login-CVPBOTvx.js +1 -0
- package/cli.js +19744 -0
- package/hook/clients.mjs +270 -0
- package/hook/hook.mjs +306 -0
- package/hook/loader.mjs +12 -0
- package/hook/rewrite.mjs +95 -0
- package/package.json +17 -3
- package/python/modelos_secure/__init__.py +0 -0
- package/python/modelos_secure/clients.py +444 -0
- package/python/modelos_secure/hook.py +542 -0
- package/python/requirements.txt +0 -0
- package/python/sitecustomize.py +13 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/web-tree-sitter.wasm +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelos-secure",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tests AI systems for Security, Safety and Quality. One command connects a system from its code.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mn-chetan/modelos-secure.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"modelos-secure": "cli.js"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
"./hook": "./hook/hook.mjs"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
}
|
|
6
20
|
}
|
|
File without changes
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""The model clients: the prompt canary going in, and what the model asked for coming out.
|
|
2
|
+
|
|
3
|
+
Whatever happens here, the app's own call goes ahead as if the hook were not there.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import contextvars
|
|
7
|
+
import functools
|
|
8
|
+
import inspect
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
_MARK = "__modelos_secure_wrapped__"
|
|
12
|
+
|
|
13
|
+
# A framework's model call is reported once, by the framework's own patch, not again by the client
|
|
14
|
+
# underneath it. A context variable follows the call through threads and tasks.
|
|
15
|
+
_inside_framework = contextvars.ContextVar("modelos_secure_inside_framework", default=False)
|
|
16
|
+
|
|
17
|
+
_canary_line = None
|
|
18
|
+
_see_model_call = None
|
|
19
|
+
_safely = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def connect(canary_line, see_model_call, safely):
|
|
23
|
+
"""What this module needs from the hook, which holds the state of the current attempt."""
|
|
24
|
+
global _canary_line, _see_model_call, _safely
|
|
25
|
+
_canary_line = canary_line
|
|
26
|
+
_see_model_call = see_model_call
|
|
27
|
+
_safely = safely
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _with_line(content, line):
|
|
31
|
+
"""Adds a line to a message's content, whether it is text or a list of parts."""
|
|
32
|
+
if isinstance(content, str):
|
|
33
|
+
return f"{content}\n{line}"
|
|
34
|
+
if isinstance(content, list):
|
|
35
|
+
return [*content, {"type": "text", "text": line}]
|
|
36
|
+
return content
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_args(text):
|
|
40
|
+
if isinstance(text, dict):
|
|
41
|
+
return text
|
|
42
|
+
try:
|
|
43
|
+
parsed = json.loads(text)
|
|
44
|
+
return parsed if isinstance(parsed, dict) else {"value": parsed}
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
return {"raw": str(text)}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _Watched:
|
|
50
|
+
"""A stream the app reads as usual. What the model asked for is known once it has been read."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, stream, reader, report):
|
|
53
|
+
self._stream = stream
|
|
54
|
+
self._reader = reader
|
|
55
|
+
self._report = report
|
|
56
|
+
self._iterator = None
|
|
57
|
+
self._done = False
|
|
58
|
+
|
|
59
|
+
def __getattr__(self, name):
|
|
60
|
+
return getattr(self._stream, name)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def __class__(self):
|
|
64
|
+
return type(self._stream)
|
|
65
|
+
|
|
66
|
+
def _see(self, chunk):
|
|
67
|
+
_safely(self._reader.add, chunk)
|
|
68
|
+
return chunk
|
|
69
|
+
|
|
70
|
+
def _finish(self):
|
|
71
|
+
if not self._done:
|
|
72
|
+
self._done = True
|
|
73
|
+
_safely(self._report, self._reader.finish())
|
|
74
|
+
|
|
75
|
+
def __iter__(self):
|
|
76
|
+
self._iterator = iter(self._stream)
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __next__(self):
|
|
80
|
+
if self._iterator is None:
|
|
81
|
+
self._iterator = iter(self._stream)
|
|
82
|
+
try:
|
|
83
|
+
return self._see(next(self._iterator))
|
|
84
|
+
except StopIteration:
|
|
85
|
+
self._finish()
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
def __aiter__(self):
|
|
89
|
+
self._iterator = self._stream.__aiter__()
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
async def __anext__(self):
|
|
93
|
+
if self._iterator is None:
|
|
94
|
+
self._iterator = self._stream.__aiter__()
|
|
95
|
+
try:
|
|
96
|
+
return self._see(await self._iterator.__anext__())
|
|
97
|
+
except StopAsyncIteration:
|
|
98
|
+
self._finish()
|
|
99
|
+
raise
|
|
100
|
+
|
|
101
|
+
def __enter__(self):
|
|
102
|
+
self._stream.__enter__()
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, *details):
|
|
106
|
+
self._finish()
|
|
107
|
+
return self._stream.__exit__(*details)
|
|
108
|
+
|
|
109
|
+
async def __aenter__(self):
|
|
110
|
+
await self._stream.__aenter__()
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
async def __aexit__(self, *details):
|
|
114
|
+
self._finish()
|
|
115
|
+
return await self._stream.__aexit__(*details)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _watch_call(original, prepare, read, stream_reader=None, always_streams=False):
|
|
119
|
+
"""Wraps a model client's method. prepare adds the canary; read says what came back."""
|
|
120
|
+
|
|
121
|
+
def before(args, kwargs):
|
|
122
|
+
if _inside_framework.get():
|
|
123
|
+
return args, kwargs, False
|
|
124
|
+
prepared = _safely(prepare, args, kwargs, fallback=None)
|
|
125
|
+
args, kwargs = prepared if prepared else (args, kwargs)
|
|
126
|
+
return args, kwargs, True
|
|
127
|
+
|
|
128
|
+
def after(args, kwargs, result):
|
|
129
|
+
def report(found):
|
|
130
|
+
calls, input_tokens, output_tokens = found
|
|
131
|
+
_see_model_call(_model_argument(args, kwargs), calls, input_tokens, output_tokens)
|
|
132
|
+
|
|
133
|
+
if stream_reader and (always_streams or kwargs.get("stream")):
|
|
134
|
+
return _Watched(result, stream_reader(), report)
|
|
135
|
+
found = _safely(read, result)
|
|
136
|
+
if found:
|
|
137
|
+
_safely(report, found)
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
if inspect.iscoroutinefunction(original):
|
|
141
|
+
|
|
142
|
+
@functools.wraps(original)
|
|
143
|
+
async def wrapper(*args, **kwargs):
|
|
144
|
+
args, kwargs, watching = before(args, kwargs)
|
|
145
|
+
result = await original(*args, **kwargs)
|
|
146
|
+
return after(args, kwargs, result) if watching else result
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
|
|
150
|
+
@functools.wraps(original)
|
|
151
|
+
def wrapper(*args, **kwargs):
|
|
152
|
+
args, kwargs, watching = before(args, kwargs)
|
|
153
|
+
result = original(*args, **kwargs)
|
|
154
|
+
if not watching:
|
|
155
|
+
return result
|
|
156
|
+
if inspect.isawaitable(result):
|
|
157
|
+
return _after_awaited(args, kwargs, result, after)
|
|
158
|
+
return after(args, kwargs, result)
|
|
159
|
+
|
|
160
|
+
setattr(wrapper, _MARK, True)
|
|
161
|
+
return wrapper
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def _after_awaited(args, kwargs, awaitable, after):
|
|
165
|
+
return after(args, kwargs, await awaitable)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _model_argument(args, kwargs):
|
|
169
|
+
model = kwargs.get("model")
|
|
170
|
+
return model if isinstance(model, str) else None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _replace(cls, attr, make):
|
|
174
|
+
original = vars(cls).get(attr)
|
|
175
|
+
if original is not None and not getattr(original, _MARK, False):
|
|
176
|
+
setattr(cls, attr, make(original))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# openai: chat completions and responses.
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _prepare_chat(args, kwargs):
|
|
183
|
+
line = _canary_line()
|
|
184
|
+
messages = kwargs.get("messages")
|
|
185
|
+
if not line or not isinstance(messages, list):
|
|
186
|
+
return args, kwargs
|
|
187
|
+
for index, message in enumerate(messages):
|
|
188
|
+
if isinstance(message, dict) and message.get("role") in ("system", "developer"):
|
|
189
|
+
changed = {**message, "content": _with_line(message.get("content"), line)}
|
|
190
|
+
return args, {**kwargs, "messages": [*messages[:index], changed, *messages[index + 1 :]]}
|
|
191
|
+
return args, kwargs
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _read_chat(response):
|
|
195
|
+
if not hasattr(response, "choices") and hasattr(response, "parse"):
|
|
196
|
+
response = response.parse()
|
|
197
|
+
calls = []
|
|
198
|
+
for choice in response.choices or []:
|
|
199
|
+
for call in getattr(choice.message, "tool_calls", None) or []:
|
|
200
|
+
function = getattr(call, "function", None)
|
|
201
|
+
if function:
|
|
202
|
+
calls.append({"tool": function.name, "args": _parse_args(function.arguments)})
|
|
203
|
+
usage = getattr(response, "usage", None)
|
|
204
|
+
return calls, getattr(usage, "prompt_tokens", 0), getattr(usage, "completion_tokens", 0)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class _ChatStreamReader:
|
|
208
|
+
def __init__(self):
|
|
209
|
+
self.calls = {}
|
|
210
|
+
self.usage = None
|
|
211
|
+
|
|
212
|
+
def add(self, chunk):
|
|
213
|
+
self.usage = getattr(chunk, "usage", None) or self.usage
|
|
214
|
+
for choice in getattr(chunk, "choices", None) or []:
|
|
215
|
+
for delta in getattr(choice.delta, "tool_calls", None) or []:
|
|
216
|
+
call = self.calls.setdefault(delta.index, {"tool": "", "text": ""})
|
|
217
|
+
function = delta.function
|
|
218
|
+
if function is not None:
|
|
219
|
+
call["tool"] += function.name or ""
|
|
220
|
+
call["text"] += function.arguments or ""
|
|
221
|
+
|
|
222
|
+
def finish(self):
|
|
223
|
+
calls = [{"tool": c["tool"], "args": _parse_args(c["text"])} for c in self.calls.values()]
|
|
224
|
+
usage = self.usage
|
|
225
|
+
return calls, getattr(usage, "prompt_tokens", 0), getattr(usage, "completion_tokens", 0)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _patch_openai_chat(module):
|
|
229
|
+
for name in ("Completions", "AsyncCompletions"):
|
|
230
|
+
cls = getattr(module, name, None)
|
|
231
|
+
if isinstance(cls, type):
|
|
232
|
+
_replace(
|
|
233
|
+
cls,
|
|
234
|
+
"create",
|
|
235
|
+
lambda original: _watch_call(original, _prepare_chat, _read_chat, _ChatStreamReader),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _prepare_responses(args, kwargs):
|
|
240
|
+
line = _canary_line()
|
|
241
|
+
if not line:
|
|
242
|
+
return args, kwargs
|
|
243
|
+
if isinstance(kwargs.get("instructions"), str):
|
|
244
|
+
return args, {**kwargs, "instructions": f"{kwargs['instructions']}\n{line}"}
|
|
245
|
+
items = kwargs.get("input")
|
|
246
|
+
if isinstance(items, list):
|
|
247
|
+
prepared = _prepare_chat(args, {**kwargs, "messages": items})
|
|
248
|
+
changed = dict(prepared[1])
|
|
249
|
+
changed["input"] = changed.pop("messages")
|
|
250
|
+
return args, changed
|
|
251
|
+
return args, kwargs
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _read_responses(response):
|
|
255
|
+
if not hasattr(response, "output") and hasattr(response, "parse"):
|
|
256
|
+
response = response.parse()
|
|
257
|
+
calls = [
|
|
258
|
+
{"tool": item.name, "args": _parse_args(item.arguments)}
|
|
259
|
+
for item in response.output or []
|
|
260
|
+
if getattr(item, "type", None) == "function_call"
|
|
261
|
+
]
|
|
262
|
+
usage = getattr(response, "usage", None)
|
|
263
|
+
return calls, getattr(usage, "input_tokens", 0), getattr(usage, "output_tokens", 0)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class _ResponsesStreamReader:
|
|
267
|
+
def __init__(self):
|
|
268
|
+
self.found = ([], 0, 0)
|
|
269
|
+
|
|
270
|
+
def add(self, event):
|
|
271
|
+
if getattr(event, "type", None) == "response.completed":
|
|
272
|
+
self.found = _read_responses(event.response)
|
|
273
|
+
|
|
274
|
+
def finish(self):
|
|
275
|
+
return self.found
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _patch_openai_responses(module):
|
|
279
|
+
for name in ("Responses", "AsyncResponses"):
|
|
280
|
+
cls = getattr(module, name, None)
|
|
281
|
+
if isinstance(cls, type):
|
|
282
|
+
_replace(
|
|
283
|
+
cls,
|
|
284
|
+
"create",
|
|
285
|
+
lambda original: _watch_call(
|
|
286
|
+
original, _prepare_responses, _read_responses, _ResponsesStreamReader
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# google.genai: every real model call, which the client's automatic function calling loops over.
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _with_instruction(instruction, line):
|
|
295
|
+
if isinstance(instruction, str):
|
|
296
|
+
return f"{instruction}\n{line}"
|
|
297
|
+
if isinstance(instruction, list):
|
|
298
|
+
return [*instruction, line]
|
|
299
|
+
parts = getattr(instruction, "parts", None)
|
|
300
|
+
if parts:
|
|
301
|
+
return instruction.model_copy(update={"parts": [*parts, type(parts[0])(text=line)]})
|
|
302
|
+
if isinstance(instruction, dict) and isinstance(instruction.get("parts"), list):
|
|
303
|
+
return {**instruction, "parts": [*instruction["parts"], {"text": line}]}
|
|
304
|
+
return instruction
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _prepare_genai(args, kwargs):
|
|
308
|
+
line = _canary_line()
|
|
309
|
+
config = kwargs.get("config")
|
|
310
|
+
if not line or config is None:
|
|
311
|
+
return args, kwargs
|
|
312
|
+
if isinstance(config, dict):
|
|
313
|
+
for key in ("system_instruction", "systemInstruction"):
|
|
314
|
+
if config.get(key):
|
|
315
|
+
changed = {**config, key: _with_instruction(config[key], line)}
|
|
316
|
+
return args, {**kwargs, "config": changed}
|
|
317
|
+
return args, kwargs
|
|
318
|
+
instruction = getattr(config, "system_instruction", None)
|
|
319
|
+
if instruction:
|
|
320
|
+
changed = config.model_copy(
|
|
321
|
+
update={"system_instruction": _with_instruction(instruction, line)}
|
|
322
|
+
)
|
|
323
|
+
return args, {**kwargs, "config": changed}
|
|
324
|
+
return args, kwargs
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _read_genai(response):
|
|
328
|
+
calls = [
|
|
329
|
+
{"tool": call.name, "args": dict(call.args or {})}
|
|
330
|
+
for call in getattr(response, "function_calls", None) or []
|
|
331
|
+
]
|
|
332
|
+
usage = getattr(response, "usage_metadata", None)
|
|
333
|
+
return (
|
|
334
|
+
calls,
|
|
335
|
+
getattr(usage, "prompt_token_count", 0),
|
|
336
|
+
getattr(usage, "candidates_token_count", 0),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class _GenAiStreamReader:
|
|
341
|
+
def __init__(self):
|
|
342
|
+
self.calls = []
|
|
343
|
+
self.usage = (0, 0)
|
|
344
|
+
|
|
345
|
+
def add(self, chunk):
|
|
346
|
+
calls, input_tokens, output_tokens = _read_genai(chunk)
|
|
347
|
+
self.calls += calls
|
|
348
|
+
self.usage = (input_tokens or self.usage[0], output_tokens or self.usage[1])
|
|
349
|
+
|
|
350
|
+
def finish(self):
|
|
351
|
+
return self.calls, self.usage[0], self.usage[1]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _patch_genai(module):
|
|
355
|
+
for name in ("Models", "AsyncModels"):
|
|
356
|
+
cls = getattr(module, name, None)
|
|
357
|
+
if not isinstance(cls, type):
|
|
358
|
+
continue
|
|
359
|
+
_replace(
|
|
360
|
+
cls, "_generate_content", lambda original: _watch_call(original, _prepare_genai, _read_genai)
|
|
361
|
+
)
|
|
362
|
+
_replace(
|
|
363
|
+
cls,
|
|
364
|
+
"_generate_content_stream",
|
|
365
|
+
lambda original: _watch_call(
|
|
366
|
+
original, _prepare_genai, _read_genai, _GenAiStreamReader, always_streams=True
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# LangChain chat models: every provider's model passes through these two.
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _prepare_langchain_messages(messages):
|
|
375
|
+
line = _canary_line()
|
|
376
|
+
if not line or not isinstance(messages, list):
|
|
377
|
+
return messages
|
|
378
|
+
for index, message in enumerate(messages):
|
|
379
|
+
if getattr(message, "type", None) == "system":
|
|
380
|
+
changed = message.model_copy(update={"content": _with_line(message.content, line)})
|
|
381
|
+
return [*messages[:index], changed, *messages[index + 1 :]]
|
|
382
|
+
return messages
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _read_langchain(model, result):
|
|
386
|
+
message = result.generations[0].message
|
|
387
|
+
calls = [
|
|
388
|
+
{"tool": call["name"], "args": call["args"]}
|
|
389
|
+
for call in getattr(message, "tool_calls", None) or []
|
|
390
|
+
]
|
|
391
|
+
usage = getattr(message, "usage_metadata", None) or {}
|
|
392
|
+
_see_model_call(model, calls, usage.get("input_tokens", 0), usage.get("output_tokens", 0))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _langchain_model_name(chat_model):
|
|
396
|
+
return getattr(chat_model, "model_name", None) or getattr(chat_model, "model", None)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _patch_langchain(module):
|
|
400
|
+
cls = getattr(module, "BaseChatModel", None)
|
|
401
|
+
if not isinstance(cls, type):
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
def sync_call(original):
|
|
405
|
+
@functools.wraps(original)
|
|
406
|
+
def wrapper(self, messages, *args, **kwargs):
|
|
407
|
+
messages = _safely(_prepare_langchain_messages, messages, fallback=messages)
|
|
408
|
+
token = _inside_framework.set(True)
|
|
409
|
+
try:
|
|
410
|
+
result = original(self, messages, *args, **kwargs)
|
|
411
|
+
finally:
|
|
412
|
+
_inside_framework.reset(token)
|
|
413
|
+
_safely(_read_langchain, _langchain_model_name(self), result)
|
|
414
|
+
return result
|
|
415
|
+
|
|
416
|
+
setattr(wrapper, _MARK, True)
|
|
417
|
+
return wrapper
|
|
418
|
+
|
|
419
|
+
def async_call(original):
|
|
420
|
+
@functools.wraps(original)
|
|
421
|
+
async def wrapper(self, messages, *args, **kwargs):
|
|
422
|
+
messages = _safely(_prepare_langchain_messages, messages, fallback=messages)
|
|
423
|
+
token = _inside_framework.set(True)
|
|
424
|
+
try:
|
|
425
|
+
result = await original(self, messages, *args, **kwargs)
|
|
426
|
+
finally:
|
|
427
|
+
_inside_framework.reset(token)
|
|
428
|
+
_safely(_read_langchain, _langchain_model_name(self), result)
|
|
429
|
+
return result
|
|
430
|
+
|
|
431
|
+
setattr(wrapper, _MARK, True)
|
|
432
|
+
return wrapper
|
|
433
|
+
|
|
434
|
+
_replace(cls, "_generate_with_cache", sync_call)
|
|
435
|
+
_replace(cls, "_agenerate_with_cache", async_call)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
LIBRARIES = {
|
|
439
|
+
"openai.resources.chat.completions.completions": _patch_openai_chat,
|
|
440
|
+
"openai.resources.chat.completions": _patch_openai_chat,
|
|
441
|
+
"openai.resources.responses.responses": _patch_openai_responses,
|
|
442
|
+
"google.genai.models": _patch_genai,
|
|
443
|
+
"langchain_core.language_models.chat_models": _patch_langchain,
|
|
444
|
+
}
|