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,417 @@
|
|
|
1
|
+
"""Model fallback middleware for agents.
|
|
2
|
+
|
|
3
|
+
When a caching middleware such as `AnthropicPromptCachingMiddleware` wraps this
|
|
4
|
+
middleware from the outside, it applies Anthropic `cache_control` markers to the
|
|
5
|
+
request *before* the fallback loop runs. Those markers are provider-specific and
|
|
6
|
+
cause API errors on non-Anthropic fallback models, so this middleware strips them
|
|
7
|
+
from fallback attempts — but only when the fallback model itself cannot accept
|
|
8
|
+
Anthropic cache markers. When the fallback is another Anthropic model the markers
|
|
9
|
+
are valid and preserve prompt caching, so they are left intact.
|
|
10
|
+
|
|
11
|
+
The knowledge of the `cache_control` marker is duplicated here (rather than owned
|
|
12
|
+
solely by the Anthropic partner package) because an outer caching middleware
|
|
13
|
+
never re-runs during fallback and therefore cannot clean up after itself.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
from coco_core.tools import BaseTool
|
|
22
|
+
from coco_graph.errors import GraphBubbleUp
|
|
23
|
+
|
|
24
|
+
from coco.agents.middleware.types import (
|
|
25
|
+
AgentMiddleware,
|
|
26
|
+
AgentState,
|
|
27
|
+
ContextT,
|
|
28
|
+
ModelRequest,
|
|
29
|
+
ModelResponse,
|
|
30
|
+
ResponseT,
|
|
31
|
+
)
|
|
32
|
+
from coco.chat_models import init_chat_model
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Awaitable, Callable
|
|
36
|
+
|
|
37
|
+
from coco_core.language_models.chat_models import BaseChatModel
|
|
38
|
+
from coco_core.messages import AIMessage, AnyMessage, SystemMessage
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _sanitize_content_blocks(
|
|
44
|
+
content: str | list[str | dict[str, Any]],
|
|
45
|
+
) -> str | list[str | dict[str, Any]]:
|
|
46
|
+
"""Remove Anthropic cache markers from message content blocks."""
|
|
47
|
+
if not isinstance(content, list):
|
|
48
|
+
return content
|
|
49
|
+
|
|
50
|
+
sanitized_content: list[str | dict[str, Any]] = []
|
|
51
|
+
changed = False
|
|
52
|
+
|
|
53
|
+
for block in content:
|
|
54
|
+
if not isinstance(block, dict):
|
|
55
|
+
sanitized_content.append(block)
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
sanitized_block, block_changed = _without_cache_control_from_content_block(block)
|
|
59
|
+
changed = changed or block_changed
|
|
60
|
+
sanitized_content.append(sanitized_block)
|
|
61
|
+
|
|
62
|
+
return sanitized_content if changed else content
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _sanitize_system_message(
|
|
66
|
+
system_message: SystemMessage | None,
|
|
67
|
+
) -> SystemMessage | None:
|
|
68
|
+
"""Remove Anthropic cache markers from a system message."""
|
|
69
|
+
if system_message is None:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
sanitized_content = _sanitize_content_blocks(system_message.content)
|
|
73
|
+
if sanitized_content is system_message.content:
|
|
74
|
+
return system_message
|
|
75
|
+
|
|
76
|
+
return system_message.model_copy(update={"content": sanitized_content})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sanitize_messages(messages: list[AnyMessage]) -> list[AnyMessage]:
|
|
80
|
+
"""Remove Anthropic cache markers from request messages."""
|
|
81
|
+
sanitized_messages: list[AnyMessage] = []
|
|
82
|
+
changed = False
|
|
83
|
+
|
|
84
|
+
for message in messages:
|
|
85
|
+
sanitized_message, message_changed = _sanitize_message(message)
|
|
86
|
+
changed = changed or message_changed
|
|
87
|
+
sanitized_messages.append(sanitized_message)
|
|
88
|
+
|
|
89
|
+
return sanitized_messages if changed else messages
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _sanitize_tools(
|
|
93
|
+
tools: list[BaseTool | dict[str, Any]],
|
|
94
|
+
) -> list[BaseTool | dict[str, Any]]:
|
|
95
|
+
"""Remove Anthropic cache markers from tool payloads."""
|
|
96
|
+
sanitized_tools: list[BaseTool | dict[str, Any]] = []
|
|
97
|
+
changed = False
|
|
98
|
+
|
|
99
|
+
for tool in tools:
|
|
100
|
+
sanitized_tool: BaseTool | dict[str, Any]
|
|
101
|
+
if isinstance(tool, BaseTool):
|
|
102
|
+
sanitized_tool, tool_changed = _sanitize_base_tool(tool)
|
|
103
|
+
else:
|
|
104
|
+
sanitized_tool, tool_changed = _sanitize_dict_tool(tool)
|
|
105
|
+
|
|
106
|
+
changed = changed or tool_changed
|
|
107
|
+
sanitized_tools.append(sanitized_tool)
|
|
108
|
+
|
|
109
|
+
return sanitized_tools if changed else tools
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _sanitize_request_for_fallback(request: ModelRequest[ContextT]) -> ModelRequest[ContextT]:
|
|
113
|
+
"""Sanitize provider-specific Anthropic cache markers before fallback attempts."""
|
|
114
|
+
overrides: dict[str, Any] = {}
|
|
115
|
+
|
|
116
|
+
model_settings, model_settings_changed = _without_cache_control(request.model_settings)
|
|
117
|
+
if model_settings_changed:
|
|
118
|
+
overrides["model_settings"] = model_settings
|
|
119
|
+
|
|
120
|
+
system_message = _sanitize_system_message(request.system_message)
|
|
121
|
+
if system_message is not request.system_message:
|
|
122
|
+
overrides["system_message"] = system_message
|
|
123
|
+
|
|
124
|
+
messages = _sanitize_messages(request.messages)
|
|
125
|
+
if messages is not request.messages:
|
|
126
|
+
overrides["messages"] = messages
|
|
127
|
+
|
|
128
|
+
tools = _sanitize_tools(request.tools)
|
|
129
|
+
if tools is not request.tools:
|
|
130
|
+
overrides["tools"] = tools
|
|
131
|
+
|
|
132
|
+
if not overrides:
|
|
133
|
+
return request
|
|
134
|
+
|
|
135
|
+
# Log only the field names that changed, never request content (may contain
|
|
136
|
+
# prompt data or PII).
|
|
137
|
+
logger.debug(
|
|
138
|
+
"Stripped Anthropic cache_control markers from %s before fallback attempt",
|
|
139
|
+
sorted(overrides),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return request.override(**overrides)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _sanitize_message(message: AnyMessage) -> tuple[AnyMessage, bool]:
|
|
146
|
+
"""Remove Anthropic cache markers from a single message.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The sanitized message (the original instance when unchanged) and whether
|
|
150
|
+
any marker was removed.
|
|
151
|
+
"""
|
|
152
|
+
sanitized_content = _sanitize_content_blocks(message.content)
|
|
153
|
+
if sanitized_content is message.content:
|
|
154
|
+
return message, False
|
|
155
|
+
|
|
156
|
+
return message.model_copy(update={"content": sanitized_content}), True
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _sanitize_base_tool(tool: BaseTool) -> tuple[BaseTool, bool]:
|
|
160
|
+
"""Remove Anthropic cache markers from a `BaseTool` payload.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The sanitized tool (the original instance when unchanged) and whether any
|
|
164
|
+
marker was removed.
|
|
165
|
+
|
|
166
|
+
Emptied `extras` collapse back to `None`.
|
|
167
|
+
"""
|
|
168
|
+
if not tool.extras:
|
|
169
|
+
return tool, False
|
|
170
|
+
|
|
171
|
+
sanitized_extras, changed = _without_cache_control(tool.extras)
|
|
172
|
+
if not changed:
|
|
173
|
+
return tool, False
|
|
174
|
+
|
|
175
|
+
return tool.model_copy(update={"extras": sanitized_extras or None}), True
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _sanitize_dict_tool(tool: dict[str, Any]) -> tuple[dict[str, Any], bool]:
|
|
179
|
+
"""Remove Anthropic cache markers from a dict-style tool payload.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
The sanitized tool (the original instance when unchanged) and whether any
|
|
183
|
+
marker was removed.
|
|
184
|
+
|
|
185
|
+
Emptied `extras` collapse back to `None`.
|
|
186
|
+
"""
|
|
187
|
+
sanitized_tool, changed = _without_cache_control(tool)
|
|
188
|
+
|
|
189
|
+
extras = sanitized_tool.get("extras")
|
|
190
|
+
if not isinstance(extras, dict):
|
|
191
|
+
return sanitized_tool, changed
|
|
192
|
+
|
|
193
|
+
sanitized_extras, extras_changed = _without_cache_control(extras)
|
|
194
|
+
if not extras_changed:
|
|
195
|
+
return sanitized_tool, changed
|
|
196
|
+
|
|
197
|
+
return {**sanitized_tool, "extras": sanitized_extras or None}, True
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _without_cache_control(payload: dict[str, Any]) -> tuple[dict[str, Any], bool]:
|
|
201
|
+
"""Return payload without `cache_control`, plus whether anything changed."""
|
|
202
|
+
if "cache_control" not in payload:
|
|
203
|
+
return payload, False
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
{key: value for key, value in payload.items() if key != "cache_control"},
|
|
207
|
+
True,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _without_cache_control_from_content_block(
|
|
212
|
+
block: dict[str, Any],
|
|
213
|
+
) -> tuple[dict[str, Any], bool]:
|
|
214
|
+
"""Return content block without Anthropic cache markers.
|
|
215
|
+
|
|
216
|
+
Strips `cache_control` from the block itself and from its nested `extras` and
|
|
217
|
+
`metadata` payloads.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The sanitized block (the original instance when unchanged) and whether any
|
|
221
|
+
marker was removed.
|
|
222
|
+
"""
|
|
223
|
+
sanitized_block, changed = _without_cache_control(block)
|
|
224
|
+
|
|
225
|
+
for nested_key in ("extras", "metadata"):
|
|
226
|
+
nested_payload = sanitized_block.get(nested_key)
|
|
227
|
+
if not isinstance(nested_payload, dict):
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
sanitized_payload, nested_changed = _without_cache_control(nested_payload)
|
|
231
|
+
if not nested_changed:
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
if sanitized_block is block:
|
|
235
|
+
sanitized_block = dict(block)
|
|
236
|
+
sanitized_block[nested_key] = sanitized_payload
|
|
237
|
+
changed = True
|
|
238
|
+
|
|
239
|
+
return sanitized_block, changed
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# `_llm_type` values that indicate a model speaks an Anthropic-compatible API
|
|
243
|
+
# and therefore accepts `cache_control` markers. Direct Anthropic models
|
|
244
|
+
# (`ChatAnthropic`) report `"anthropic-chat"`; Bedrock-hosted Claude
|
|
245
|
+
# (`ChatAnthropicBedrock`, a `ChatAnthropic` subclass in `trustcoco-aws`) reports
|
|
246
|
+
# `"anthropic-bedrock-chat"` and translates the top-level kwarg into block-level
|
|
247
|
+
# breakpoints inside the inherited `ChatAnthropic._get_request_payload`, while
|
|
248
|
+
# content-block and tool `cache_control` markers pass through unchanged.
|
|
249
|
+
# Vertex-hosted Claude (`ChatAnthropicVertex` in `trustcoco-google`) reports
|
|
250
|
+
# `"anthropic-chat-vertexai"` and nests the same marker shape through its own
|
|
251
|
+
# request builder — not the shared `ChatAnthropic` method. All three keep prompt
|
|
252
|
+
# caching intact on fallback.
|
|
253
|
+
#
|
|
254
|
+
# Keep this set in sync with those classes' `_llm_type` values, which live in
|
|
255
|
+
# separate repositories. If a value drifts or a new Anthropic transport ships,
|
|
256
|
+
# the failure mode is silent loss of prompt caching (markers stripped from a
|
|
257
|
+
# model that supports them), not a hard error — so CI here will not catch it.
|
|
258
|
+
_ANTHROPIC_LLM_TYPES: frozenset[str] = frozenset(
|
|
259
|
+
{
|
|
260
|
+
"anthropic-chat",
|
|
261
|
+
"anthropic-bedrock-chat",
|
|
262
|
+
"anthropic-chat-vertexai",
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _supports_anthropic_cache_control(model: BaseChatModel) -> bool:
|
|
268
|
+
"""Return whether `model` accepts Anthropic `cache_control` markers.
|
|
269
|
+
|
|
270
|
+
Checked via `_llm_type` so the decision is provider-based rather than
|
|
271
|
+
model-name-based: any Anthropic-compatible model (including future model IDs
|
|
272
|
+
we have not seen) keeps its cache markers on fallback, while OpenAI, Gemini,
|
|
273
|
+
and other non-Anthropic providers get a sanitized request.
|
|
274
|
+
"""
|
|
275
|
+
llm_type = getattr(model, "_llm_type", None)
|
|
276
|
+
return isinstance(llm_type, str) and llm_type in _ANTHROPIC_LLM_TYPES
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class ModelFallbackMiddleware(AgentMiddleware[AgentState[ResponseT], ContextT, ResponseT]):
|
|
280
|
+
"""Automatic fallback to alternative models on errors.
|
|
281
|
+
|
|
282
|
+
Retries failed model calls with alternative models in sequence until
|
|
283
|
+
success or all models exhausted. Primary model specified in `create_agent`.
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
```python
|
|
287
|
+
from coco.agents.middleware import ModelFallbackMiddleware
|
|
288
|
+
from coco.agents import create_agent
|
|
289
|
+
|
|
290
|
+
fallback = ModelFallbackMiddleware(
|
|
291
|
+
"openai:gpt-5.5", # Try first on error
|
|
292
|
+
"anthropic:claude-sonnet-4-5-20250929", # Then this
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
agent = create_agent(
|
|
296
|
+
model="openai:gpt-5.5", # Primary model
|
|
297
|
+
middleware=[fallback],
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# If primary fails: tries gpt-5.5, then claude-sonnet-4-5-20250929
|
|
301
|
+
result = await agent.invoke({"messages": [HumanMessage("Hello")]})
|
|
302
|
+
```
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
def __init__(
|
|
306
|
+
self,
|
|
307
|
+
first_model: str | BaseChatModel,
|
|
308
|
+
*additional_models: str | BaseChatModel,
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Initialize model fallback middleware.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
first_model: First fallback model (string name or instance).
|
|
314
|
+
*additional_models: Additional fallbacks in order.
|
|
315
|
+
"""
|
|
316
|
+
super().__init__()
|
|
317
|
+
|
|
318
|
+
# Initialize all fallback models
|
|
319
|
+
all_models = (first_model, *additional_models)
|
|
320
|
+
self.models: list[BaseChatModel] = []
|
|
321
|
+
for model in all_models:
|
|
322
|
+
if isinstance(model, str):
|
|
323
|
+
self.models.append(init_chat_model(model))
|
|
324
|
+
else:
|
|
325
|
+
self.models.append(model)
|
|
326
|
+
|
|
327
|
+
def wrap_model_call(
|
|
328
|
+
self,
|
|
329
|
+
request: ModelRequest[ContextT],
|
|
330
|
+
handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
|
|
331
|
+
) -> ModelResponse[ResponseT] | AIMessage:
|
|
332
|
+
"""Try fallback models in sequence on errors.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
request: Initial model request.
|
|
336
|
+
handler: Callback to execute the model.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
AIMessage from successful model call.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
Exception: If all models fail, re-raises last exception.
|
|
343
|
+
"""
|
|
344
|
+
# Try primary model first
|
|
345
|
+
last_exception: Exception
|
|
346
|
+
try:
|
|
347
|
+
return handler(request)
|
|
348
|
+
except GraphBubbleUp:
|
|
349
|
+
raise
|
|
350
|
+
except Exception as e:
|
|
351
|
+
last_exception = e
|
|
352
|
+
|
|
353
|
+
# Try fallback models — sanitize cache markers only when the fallback
|
|
354
|
+
# model cannot accept them (i.e. is not an Anthropic-compatible model).
|
|
355
|
+
# The request is derived outside the try so a sanitizer or `_llm_type`
|
|
356
|
+
# bug surfaces directly instead of being masked as a model failure.
|
|
357
|
+
for fallback_model in self.models:
|
|
358
|
+
fallback_request = (
|
|
359
|
+
request
|
|
360
|
+
if _supports_anthropic_cache_control(fallback_model)
|
|
361
|
+
else _sanitize_request_for_fallback(request)
|
|
362
|
+
)
|
|
363
|
+
try:
|
|
364
|
+
return handler(fallback_request.override(model=fallback_model))
|
|
365
|
+
except GraphBubbleUp:
|
|
366
|
+
raise
|
|
367
|
+
except Exception as e:
|
|
368
|
+
last_exception = e
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
raise last_exception
|
|
372
|
+
|
|
373
|
+
async def awrap_model_call(
|
|
374
|
+
self,
|
|
375
|
+
request: ModelRequest[ContextT],
|
|
376
|
+
handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]],
|
|
377
|
+
) -> ModelResponse[ResponseT] | AIMessage:
|
|
378
|
+
"""Try fallback models in sequence on errors (async version).
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
request: Initial model request.
|
|
382
|
+
handler: Async callback to execute the model.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
AIMessage from successful model call.
|
|
386
|
+
|
|
387
|
+
Raises:
|
|
388
|
+
Exception: If all models fail, re-raises last exception.
|
|
389
|
+
"""
|
|
390
|
+
# Try primary model first
|
|
391
|
+
last_exception: Exception
|
|
392
|
+
try:
|
|
393
|
+
return await handler(request)
|
|
394
|
+
except GraphBubbleUp:
|
|
395
|
+
raise
|
|
396
|
+
except Exception as e:
|
|
397
|
+
last_exception = e
|
|
398
|
+
|
|
399
|
+
# Try fallback models — sanitize cache markers only when the fallback
|
|
400
|
+
# model cannot accept them (i.e. is not an Anthropic-compatible model).
|
|
401
|
+
# The request is derived outside the try so a sanitizer or `_llm_type`
|
|
402
|
+
# bug surfaces directly instead of being masked as a model failure.
|
|
403
|
+
for fallback_model in self.models:
|
|
404
|
+
fallback_request = (
|
|
405
|
+
request
|
|
406
|
+
if _supports_anthropic_cache_control(fallback_model)
|
|
407
|
+
else _sanitize_request_for_fallback(request)
|
|
408
|
+
)
|
|
409
|
+
try:
|
|
410
|
+
return await handler(fallback_request.override(model=fallback_model))
|
|
411
|
+
except GraphBubbleUp:
|
|
412
|
+
raise
|
|
413
|
+
except Exception as e:
|
|
414
|
+
last_exception = e
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
raise last_exception
|