coco-framework 0.1.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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Private helpers for resolving LangSmith gateway configuration.
|
|
2
|
+
|
|
3
|
+
The [LangSmith LLM gateway](https://docs.langchain.com/langsmith/llm-gateway)
|
|
4
|
+
lets a chat model reach a provider through a proxy configured via environment
|
|
5
|
+
variables. These helpers centralize the (non-trivial) precedence rules so that
|
|
6
|
+
each provider integration resolves its base URL and API key identically.
|
|
7
|
+
|
|
8
|
+
This module is private: the API is not stable and may change without notice.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
17
|
+
|
|
18
|
+
from pydantic import SecretStr
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel
|
|
24
|
+
|
|
25
|
+
# Key under which integrations place parsed gateway metadata on a generation's
|
|
26
|
+
# `generation_info`. This is the contract consumed by the LangSmith tracer, which
|
|
27
|
+
# promotes it to run metadata. Defined here so integrations and the tracer agree
|
|
28
|
+
# on a single name.
|
|
29
|
+
GATEWAY_METADATA_RESPONSE_KEY = "lc_gateway_metadata"
|
|
30
|
+
|
|
31
|
+
# HTTP response header the LangSmith gateway uses to return per-request metadata
|
|
32
|
+
# (e.g. the resolved provider and model). The value is JSON-encoded.
|
|
33
|
+
GATEWAY_METADATA_HEADER = "X-LangSmith-Gateway-Metadata"
|
|
34
|
+
|
|
35
|
+
_LANGSMITH_GATEWAY_ENV = "LANGSMITH_GATEWAY"
|
|
36
|
+
_LANGSMITH_GATEWAY_API_KEY_ENV = "LANGSMITH_GATEWAY_API_KEY"
|
|
37
|
+
_LANGSMITH_API_KEY_ENV = "LANGSMITH_API_KEY"
|
|
38
|
+
_LANGSMITH_GATEWAY_DEFAULT_BASE = "https://gateway.smith.langchain.com"
|
|
39
|
+
|
|
40
|
+
_TRUE_VALUES = ("true", "1", "yes")
|
|
41
|
+
_FALSE_VALUES = ("false", "0", "no")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GatewayConfig(NamedTuple):
|
|
45
|
+
"""Resolved gateway configuration.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
base_url: The base URL the client should use, or None to defer to the
|
|
49
|
+
provider SDK's own default.
|
|
50
|
+
api_key: The API key to use. A ``SecretStr`` when derived from the
|
|
51
|
+
environment; the caller's value returned unchanged when one was
|
|
52
|
+
passed explicitly; None when no key could be resolved.
|
|
53
|
+
base_url_from_gateway: Whether ``base_url`` was populated from the
|
|
54
|
+
LangSmith gateway (as opposed to an explicit value, a provider env
|
|
55
|
+
var, or a default).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
base_url: str | None
|
|
59
|
+
api_key: Any
|
|
60
|
+
base_url_from_gateway: bool
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _first_env(names: str | Sequence[str]) -> str | None:
|
|
64
|
+
"""Return the first non-empty value among ``names``, or None."""
|
|
65
|
+
if isinstance(names, str):
|
|
66
|
+
names = (names,)
|
|
67
|
+
for name in names:
|
|
68
|
+
value = os.getenv(name)
|
|
69
|
+
if value:
|
|
70
|
+
return value
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _resolve_gateway_base_url(provider_path: str) -> str | None:
|
|
75
|
+
"""Resolve the LangSmith gateway base URL for a provider.
|
|
76
|
+
|
|
77
|
+
``LANGSMITH_GATEWAY`` accepts either a boolean-ish string or an explicit
|
|
78
|
+
base URL:
|
|
79
|
+
|
|
80
|
+
- ``true`` / ``1`` / ``yes`` -> the default gateway host.
|
|
81
|
+
- ``false`` / ``0`` / ``no`` / unset / empty -> the gateway is disabled
|
|
82
|
+
(None).
|
|
83
|
+
- anything else -> treated as a custom gateway base URL.
|
|
84
|
+
|
|
85
|
+
The provider-specific path is appended in all enabled cases.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
provider_path: Path segment for the provider, e.g. ``"openai/v1"`` or
|
|
89
|
+
``"anthropic"``.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The provider base URL on the gateway, or None if the gateway is
|
|
93
|
+
disabled.
|
|
94
|
+
"""
|
|
95
|
+
raw = os.getenv(_LANGSMITH_GATEWAY_ENV)
|
|
96
|
+
if not raw or raw.lower() in _FALSE_VALUES:
|
|
97
|
+
return None
|
|
98
|
+
base = (
|
|
99
|
+
_LANGSMITH_GATEWAY_DEFAULT_BASE
|
|
100
|
+
if raw.lower() in _TRUE_VALUES
|
|
101
|
+
else raw.rstrip("/")
|
|
102
|
+
)
|
|
103
|
+
return f"{base}/{provider_path}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resolve_gateway_config(
|
|
107
|
+
*,
|
|
108
|
+
base_url: str | None,
|
|
109
|
+
api_key: Any,
|
|
110
|
+
provider_path: str,
|
|
111
|
+
base_url_env: str | Sequence[str] = (),
|
|
112
|
+
api_key_env: str | Sequence[str] = (),
|
|
113
|
+
default_base_url: str | None = None,
|
|
114
|
+
) -> GatewayConfig:
|
|
115
|
+
"""Resolve a provider's base URL and API key, applying gateway settings.
|
|
116
|
+
|
|
117
|
+
Precedence:
|
|
118
|
+
|
|
119
|
+
- **base_url:** explicit ``base_url`` > ``base_url_env`` > LangSmith gateway
|
|
120
|
+
> ``default_base_url``.
|
|
121
|
+
- **api_key:** explicit ``api_key`` (returned unchanged) > the gateway key if
|
|
122
|
+
the base URL came from the gateway, otherwise the provider key > the other.
|
|
123
|
+
The gateway key uses ``LANGSMITH_GATEWAY_API_KEY`` when the gateway is
|
|
124
|
+
enabled, with ``LANGSMITH_API_KEY`` as a fallback only when the base URL came
|
|
125
|
+
from the gateway.
|
|
126
|
+
|
|
127
|
+
The provenance flip on the key means an ``OPENAI_API_KEY``-style provider key
|
|
128
|
+
is preferred whenever the caller pointed the base URL at a non-gateway
|
|
129
|
+
endpoint (so a stray gateway key is not sent to the provider), while the
|
|
130
|
+
gateway key wins for the common "just enable the gateway" setup even if a
|
|
131
|
+
provider key happens to be present in the environment.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
base_url: Explicitly-provided base URL (e.g. a ``base_url`` kwarg), or
|
|
135
|
+
None if not set by the caller.
|
|
136
|
+
api_key: Explicitly-provided API key, or None if not set by the caller.
|
|
137
|
+
Returned unchanged when not None, so a caller-supplied value (secret
|
|
138
|
+
or callable) always wins.
|
|
139
|
+
provider_path: Path segment appended to the gateway host.
|
|
140
|
+
base_url_env: Env var name(s) for the provider base URL, in priority
|
|
141
|
+
order.
|
|
142
|
+
api_key_env: Env var name(s) for the provider API key, in priority order.
|
|
143
|
+
default_base_url: Base URL used when nothing else is set.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The resolved `GatewayConfig`.
|
|
147
|
+
"""
|
|
148
|
+
gateway_base_url = _resolve_gateway_base_url(provider_path)
|
|
149
|
+
|
|
150
|
+
resolved_base_url = base_url
|
|
151
|
+
base_url_from_gateway = False
|
|
152
|
+
if resolved_base_url is None:
|
|
153
|
+
resolved_base_url = _first_env(base_url_env)
|
|
154
|
+
if resolved_base_url is None:
|
|
155
|
+
if gateway_base_url is not None:
|
|
156
|
+
resolved_base_url = gateway_base_url
|
|
157
|
+
base_url_from_gateway = True
|
|
158
|
+
else:
|
|
159
|
+
resolved_base_url = default_base_url
|
|
160
|
+
|
|
161
|
+
if api_key is not None:
|
|
162
|
+
resolved_api_key: Any = api_key
|
|
163
|
+
else:
|
|
164
|
+
gateway_api_key = (
|
|
165
|
+
os.getenv(_LANGSMITH_GATEWAY_API_KEY_ENV)
|
|
166
|
+
if gateway_base_url is not None
|
|
167
|
+
else None
|
|
168
|
+
)
|
|
169
|
+
if not gateway_api_key and base_url_from_gateway:
|
|
170
|
+
gateway_api_key = os.getenv(_LANGSMITH_API_KEY_ENV)
|
|
171
|
+
provider_api_key = _first_env(api_key_env)
|
|
172
|
+
chosen = (
|
|
173
|
+
(gateway_api_key or provider_api_key)
|
|
174
|
+
if base_url_from_gateway
|
|
175
|
+
else (provider_api_key or gateway_api_key)
|
|
176
|
+
)
|
|
177
|
+
resolved_api_key = SecretStr(chosen) if chosen else None
|
|
178
|
+
|
|
179
|
+
return GatewayConfig(resolved_base_url, resolved_api_key, base_url_from_gateway)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _pop_provided(values: dict[str, Any], cls: type[BaseModel], field: str) -> Any:
|
|
183
|
+
"""Pop a caller-provided field value by name or alias; None if absent.
|
|
184
|
+
|
|
185
|
+
Handles models with ``populate_by_name=True``, where a field may be supplied
|
|
186
|
+
under either its name or its alias. Both keys are removed so the resolved
|
|
187
|
+
value can be written back canonically under the field name. The alias is read
|
|
188
|
+
from the model rather than hard-coded, so callers pass only field names.
|
|
189
|
+
"""
|
|
190
|
+
value = values.pop(field, None)
|
|
191
|
+
alias = cls.model_fields[field].alias
|
|
192
|
+
if alias is not None:
|
|
193
|
+
alias_value = values.pop(alias, None)
|
|
194
|
+
if value is None:
|
|
195
|
+
value = alias_value
|
|
196
|
+
return value
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _apply_gateway_config(
|
|
200
|
+
values: dict[str, Any],
|
|
201
|
+
cls: type[BaseModel],
|
|
202
|
+
*,
|
|
203
|
+
base_url_field: str,
|
|
204
|
+
api_key_field: str,
|
|
205
|
+
provider_path: str,
|
|
206
|
+
base_url_env: str | Sequence[str] = (),
|
|
207
|
+
api_key_env: str | Sequence[str] = (),
|
|
208
|
+
default_base_url: str | None = None,
|
|
209
|
+
) -> GatewayConfig:
|
|
210
|
+
"""Resolve gateway settings from a model's raw input in a "before" validator.
|
|
211
|
+
|
|
212
|
+
Reads the caller-provided base URL and API key (by field name or alias),
|
|
213
|
+
resolves them against the gateway and provider env vars via
|
|
214
|
+
`_resolve_gateway_config`, then writes the results back into ``values`` under
|
|
215
|
+
the canonical field names. This lets a provider integration keep a
|
|
216
|
+
non-optional key field: the resolved value is injected before field
|
|
217
|
+
validation runs, so the field always receives a concrete value.
|
|
218
|
+
|
|
219
|
+
The resolved API key is written only when non-None, so the field's own
|
|
220
|
+
default applies when no key is found. Returns the `GatewayConfig` so the
|
|
221
|
+
caller can, for example, raise when a required key is missing.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
values: The raw input mapping passed to the model, mutated in place.
|
|
225
|
+
cls: The model class, used to look up field aliases.
|
|
226
|
+
base_url_field: Name of the base URL field (e.g. ``"anthropic_api_url"``).
|
|
227
|
+
api_key_field: Name of the API key field (e.g. ``"anthropic_api_key"``).
|
|
228
|
+
provider_path: Path segment appended to the gateway host.
|
|
229
|
+
base_url_env: Env var name(s) for the provider base URL.
|
|
230
|
+
api_key_env: Env var name(s) for the provider API key.
|
|
231
|
+
default_base_url: Base URL used when nothing else is set.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The resolved `GatewayConfig`.
|
|
235
|
+
"""
|
|
236
|
+
config = _resolve_gateway_config(
|
|
237
|
+
base_url=_pop_provided(values, cls, base_url_field),
|
|
238
|
+
api_key=_pop_provided(values, cls, api_key_field),
|
|
239
|
+
provider_path=provider_path,
|
|
240
|
+
base_url_env=base_url_env,
|
|
241
|
+
api_key_env=api_key_env,
|
|
242
|
+
default_base_url=default_base_url,
|
|
243
|
+
)
|
|
244
|
+
values[base_url_field] = config.base_url
|
|
245
|
+
if config.api_key is not None:
|
|
246
|
+
values[api_key_field] = config.api_key
|
|
247
|
+
return config
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _parse_gateway_metadata(headers: Any) -> dict[str, Any] | None:
|
|
251
|
+
"""Parse LangSmith gateway metadata from HTTP response headers.
|
|
252
|
+
|
|
253
|
+
Provider integrations call this on the raw response headers so the parsed
|
|
254
|
+
result can be surfaced under `GATEWAY_METADATA_RESPONSE_KEY` and later
|
|
255
|
+
promoted to run metadata by the LangSmith tracer.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
headers: The response headers.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
The deserialized gateway metadata, or None when the header is absent,
|
|
262
|
+
not a mapping, or not a JSON object.
|
|
263
|
+
"""
|
|
264
|
+
if not isinstance(headers, Mapping):
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
raw = headers.get(GATEWAY_METADATA_HEADER)
|
|
268
|
+
if not raw:
|
|
269
|
+
return None
|
|
270
|
+
try:
|
|
271
|
+
parsed = json.loads(raw)
|
|
272
|
+
except (json.JSONDecodeError, TypeError):
|
|
273
|
+
return None
|
|
274
|
+
return parsed if isinstance(parsed, dict) else None
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def merge_dicts(left: dict[str, Any], *others: dict[str, Any]) -> dict[str, Any]:
|
|
7
|
+
r"""Merge dictionaries.
|
|
8
|
+
|
|
9
|
+
Merge many dicts, handling specific scenarios where a key exists in both
|
|
10
|
+
dictionaries but has a value of `None` in `'left'`. In such cases, the method uses
|
|
11
|
+
the value from `'right'` for that key in the merged dictionary.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
left: The first dictionary to merge.
|
|
15
|
+
others: The other dictionaries to merge.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The merged dictionary.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
TypeError: If the key exists in both dictionaries but has a different type.
|
|
22
|
+
TypeError: If the value has an unsupported type.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
If `left = {"function_call": {"arguments": None}}` and
|
|
26
|
+
`right = {"function_call": {"arguments": "{\n"}}`, then, after merging, for the
|
|
27
|
+
key `'function_call'`, the value from `'right'` is used, resulting in
|
|
28
|
+
`merged = {"function_call": {"arguments": "{\n"}}`.
|
|
29
|
+
"""
|
|
30
|
+
merged = left.copy()
|
|
31
|
+
for right in others:
|
|
32
|
+
for right_k, right_v in right.items():
|
|
33
|
+
if right_k not in merged or (
|
|
34
|
+
right_v is not None and merged[right_k] is None
|
|
35
|
+
):
|
|
36
|
+
merged[right_k] = right_v
|
|
37
|
+
elif right_v is None:
|
|
38
|
+
continue
|
|
39
|
+
elif type(merged[right_k]) is not type(right_v):
|
|
40
|
+
msg = (
|
|
41
|
+
f'additional_kwargs["{right_k}"] already exists in this message,'
|
|
42
|
+
" but with a different type."
|
|
43
|
+
)
|
|
44
|
+
raise TypeError(msg)
|
|
45
|
+
elif isinstance(merged[right_k], str):
|
|
46
|
+
# TODO: Add below special handling for 'type' key in 0.3 and remove
|
|
47
|
+
# merge_lists 'type' logic.
|
|
48
|
+
#
|
|
49
|
+
# if right_k == "type":
|
|
50
|
+
# if merged[right_k] == right_v:
|
|
51
|
+
# continue
|
|
52
|
+
# else:
|
|
53
|
+
# raise ValueError(
|
|
54
|
+
# "Unable to merge. Two different values seen for special "
|
|
55
|
+
# f"key 'type': {merged[right_k]} and {right_v}. 'type' "
|
|
56
|
+
# "should either occur once or have the same value across "
|
|
57
|
+
# "all dicts."
|
|
58
|
+
# )
|
|
59
|
+
if (right_k == "index" and merged[right_k].startswith("lc_")) or (
|
|
60
|
+
right_k in {"id", "output_version", "model_provider"}
|
|
61
|
+
and merged[right_k] == right_v
|
|
62
|
+
):
|
|
63
|
+
continue
|
|
64
|
+
merged[right_k] += right_v
|
|
65
|
+
elif isinstance(merged[right_k], dict):
|
|
66
|
+
merged[right_k] = merge_dicts(merged[right_k], right_v)
|
|
67
|
+
elif isinstance(merged[right_k], list):
|
|
68
|
+
merged[right_k] = merge_lists(merged[right_k], right_v)
|
|
69
|
+
elif merged[right_k] == right_v:
|
|
70
|
+
continue
|
|
71
|
+
elif isinstance(merged[right_k], bool):
|
|
72
|
+
# `bool` is a subclass of `int`, so without this check differing
|
|
73
|
+
# booleans would fall into the int branch below and get summed,
|
|
74
|
+
# silently turning e.g. `True + False` into the int `1`.
|
|
75
|
+
msg = (
|
|
76
|
+
f"Additional kwargs key {right_k} already exists in left dict and "
|
|
77
|
+
f"value has unsupported type {type(merged[right_k])}."
|
|
78
|
+
)
|
|
79
|
+
raise TypeError(msg)
|
|
80
|
+
elif isinstance(merged[right_k], int):
|
|
81
|
+
# Preserve identification and temporal fields using last-wins strategy
|
|
82
|
+
# instead of summing:
|
|
83
|
+
# - index: identifies which tool call a chunk belongs to
|
|
84
|
+
# - created/timestamp: temporal values that shouldn't be accumulated
|
|
85
|
+
if right_k in {"index", "created", "timestamp"}:
|
|
86
|
+
merged[right_k] = right_v
|
|
87
|
+
else:
|
|
88
|
+
merged[right_k] += right_v
|
|
89
|
+
else:
|
|
90
|
+
msg = (
|
|
91
|
+
f"Additional kwargs key {right_k} already exists in left dict and "
|
|
92
|
+
f"value has unsupported type {type(merged[right_k])}."
|
|
93
|
+
)
|
|
94
|
+
raise TypeError(msg)
|
|
95
|
+
return merged
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def merge_lists(left: list[Any] | None, *others: list[Any] | None) -> list[Any] | None:
|
|
99
|
+
"""Add many lists, handling `None`.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
left: The first list to merge.
|
|
103
|
+
others: The other lists to merge.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The merged list.
|
|
107
|
+
"""
|
|
108
|
+
merged = left.copy() if left is not None else None
|
|
109
|
+
for other in others:
|
|
110
|
+
if other is None:
|
|
111
|
+
continue
|
|
112
|
+
if merged is None:
|
|
113
|
+
merged = other.copy()
|
|
114
|
+
else:
|
|
115
|
+
for e in other:
|
|
116
|
+
if (
|
|
117
|
+
isinstance(e, dict)
|
|
118
|
+
and "index" in e
|
|
119
|
+
and (
|
|
120
|
+
isinstance(e["index"], int)
|
|
121
|
+
or (
|
|
122
|
+
isinstance(e["index"], str) and e["index"].startswith("lc_")
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
):
|
|
126
|
+
to_merge = [
|
|
127
|
+
i
|
|
128
|
+
for i, e_left in enumerate(merged)
|
|
129
|
+
if (
|
|
130
|
+
isinstance(e_left, dict)
|
|
131
|
+
and "index" in e_left
|
|
132
|
+
and e_left["index"] == e["index"] # index matches
|
|
133
|
+
and ( # IDs not inconsistent
|
|
134
|
+
e_left.get("id") in {None, ""}
|
|
135
|
+
or e.get("id") in {None, ""}
|
|
136
|
+
or e_left.get("id") == e.get("id")
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
]
|
|
140
|
+
if to_merge:
|
|
141
|
+
# TODO: Remove this once merge_dict is updated with special
|
|
142
|
+
# handling for 'type'.
|
|
143
|
+
if (left_type := merged[to_merge[0]].get("type")) and (
|
|
144
|
+
e.get("type") == "non_standard" and "value" in e
|
|
145
|
+
):
|
|
146
|
+
if left_type != "non_standard":
|
|
147
|
+
# standard + non_standard
|
|
148
|
+
new_e: dict[str, Any] = {
|
|
149
|
+
"extras": {
|
|
150
|
+
k: v
|
|
151
|
+
for k, v in e["value"].items()
|
|
152
|
+
if k != "type"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else:
|
|
156
|
+
# non_standard + non_standard
|
|
157
|
+
new_e = {
|
|
158
|
+
"value": {
|
|
159
|
+
k: v
|
|
160
|
+
for k, v in e["value"].items()
|
|
161
|
+
if k != "type"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if "index" in e:
|
|
165
|
+
new_e["index"] = e["index"]
|
|
166
|
+
else:
|
|
167
|
+
new_e = (
|
|
168
|
+
{k: v for k, v in e.items() if k != "type"}
|
|
169
|
+
if "type" in e
|
|
170
|
+
else e
|
|
171
|
+
)
|
|
172
|
+
merged[to_merge[0]] = merge_dicts(merged[to_merge[0]], new_e)
|
|
173
|
+
else:
|
|
174
|
+
merged.append(e)
|
|
175
|
+
else:
|
|
176
|
+
merged.append(e)
|
|
177
|
+
return merged
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def merge_obj(left: Any, right: Any) -> Any:
|
|
181
|
+
"""Merge two objects.
|
|
182
|
+
|
|
183
|
+
It handles specific scenarios where a key exists in both dictionaries but has a
|
|
184
|
+
value of `None` in `'left'`. In such cases, the method uses the value from `'right'`
|
|
185
|
+
for that key in the merged dictionary.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
left: The first object to merge.
|
|
189
|
+
right: The other object to merge.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
The merged object.
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
TypeError: If the key exists in both dictionaries but has a different type.
|
|
196
|
+
ValueError: If the two objects cannot be merged.
|
|
197
|
+
"""
|
|
198
|
+
if left is None or right is None:
|
|
199
|
+
return left if left is not None else right
|
|
200
|
+
if type(left) is not type(right):
|
|
201
|
+
msg = (
|
|
202
|
+
f"left and right are of different types. Left type: {type(left)}. Right "
|
|
203
|
+
f"type: {type(right)}."
|
|
204
|
+
)
|
|
205
|
+
raise TypeError(msg)
|
|
206
|
+
if isinstance(left, str):
|
|
207
|
+
return left + right
|
|
208
|
+
if isinstance(left, dict):
|
|
209
|
+
return merge_dicts(left, right)
|
|
210
|
+
if isinstance(left, list):
|
|
211
|
+
return merge_lists(left, right)
|
|
212
|
+
if left == right:
|
|
213
|
+
return left
|
|
214
|
+
msg = (
|
|
215
|
+
f"Unable to merge {left=} and {right=}. Both must be of type str, dict, or "
|
|
216
|
+
f"list, or else be two equal objects."
|
|
217
|
+
)
|
|
218
|
+
raise ValueError(msg)
|