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.
Files changed (400) hide show
  1. coco/__init__.py +3 -0
  2. coco/agents/__init__.py +9 -0
  3. coco/agents/_subagent_transformer.py +307 -0
  4. coco/agents/factory.py +2164 -0
  5. coco/agents/middleware/__init__.py +100 -0
  6. coco/agents/middleware/_execution.py +385 -0
  7. coco/agents/middleware/_redaction.py +454 -0
  8. coco/agents/middleware/_retry.py +134 -0
  9. coco/agents/middleware/_trace_policy.py +61 -0
  10. coco/agents/middleware/context_editing.py +306 -0
  11. coco/agents/middleware/file_search.py +433 -0
  12. coco/agents/middleware/human_in_the_loop.py +680 -0
  13. coco/agents/middleware/internal_call_transformer.py +108 -0
  14. coco/agents/middleware/model_call_limit.py +267 -0
  15. coco/agents/middleware/model_fallback.py +417 -0
  16. coco/agents/middleware/model_retry.py +327 -0
  17. coco/agents/middleware/pii.py +878 -0
  18. coco/agents/middleware/provider_tool_search.py +307 -0
  19. coco/agents/middleware/shell_tool.py +949 -0
  20. coco/agents/middleware/summarization.py +910 -0
  21. coco/agents/middleware/todo.py +357 -0
  22. coco/agents/middleware/tool_call_limit.py +495 -0
  23. coco/agents/middleware/tool_emulator.py +251 -0
  24. coco/agents/middleware/tool_error.py +209 -0
  25. coco/agents/middleware/tool_retry.py +421 -0
  26. coco/agents/middleware/tool_selection.py +515 -0
  27. coco/agents/middleware/types.py +2201 -0
  28. coco/agents/structured_output.py +472 -0
  29. coco/chat_models/__init__.py +7 -0
  30. coco/chat_models/base.py +1082 -0
  31. coco/embeddings/__init__.py +18 -0
  32. coco/embeddings/base.py +279 -0
  33. coco/mcp/__init__.py +36 -0
  34. coco/mcp/adapter.py +245 -0
  35. coco/mcp/elicitation.py +394 -0
  36. coco/mcp/tools.py +307 -0
  37. coco/messages/__init__.py +73 -0
  38. coco/py.typed +0 -0
  39. coco/rate_limiters/__init__.py +13 -0
  40. coco/tools/__init__.py +22 -0
  41. coco/tools/tool_node.py +20 -0
  42. coco_anthropic/__init__.py +15 -0
  43. coco_anthropic/_client_utils.py +81 -0
  44. coco_anthropic/_compat.py +228 -0
  45. coco_anthropic/_sdk_compat.py +124 -0
  46. coco_anthropic/_version.py +3 -0
  47. coco_anthropic/chat_models.py +3349 -0
  48. coco_anthropic/data/__init__.py +1 -0
  49. coco_anthropic/data/_profiles.py +491 -0
  50. coco_anthropic/data/profile_augmentations.toml +69 -0
  51. coco_anthropic/experimental.py +140 -0
  52. coco_anthropic/llms.py +428 -0
  53. coco_anthropic/middleware/__init__.py +25 -0
  54. coco_anthropic/middleware/anthropic_tools.py +1197 -0
  55. coco_anthropic/middleware/bash.py +84 -0
  56. coco_anthropic/middleware/file_search.py +360 -0
  57. coco_anthropic/middleware/prompt_caching.py +276 -0
  58. coco_anthropic/output_parsers.py +93 -0
  59. coco_anthropic/py.typed +0 -0
  60. coco_core/__init__.py +20 -0
  61. coco_core/_api/__init__.py +87 -0
  62. coco_core/_api/beta_decorator.py +266 -0
  63. coco_core/_api/deprecation.py +637 -0
  64. coco_core/_api/internal.py +23 -0
  65. coco_core/_api/path.py +50 -0
  66. coco_core/_import_utils.py +41 -0
  67. coco_core/_protocol.py +625 -0
  68. coco_core/_security/__init__.py +36 -0
  69. coco_core/_security/_exceptions.py +9 -0
  70. coco_core/_security/_policy.py +313 -0
  71. coco_core/_security/_ssrf_protection.py +155 -0
  72. coco_core/_security/_transport.py +254 -0
  73. coco_core/agents.py +258 -0
  74. coco_core/caches.py +272 -0
  75. coco_core/callbacks/__init__.py +132 -0
  76. coco_core/callbacks/base.py +1229 -0
  77. coco_core/callbacks/file.py +266 -0
  78. coco_core/callbacks/manager.py +2826 -0
  79. coco_core/callbacks/stdout.py +123 -0
  80. coco_core/callbacks/streaming_stdout.py +154 -0
  81. coco_core/callbacks/usage.py +124 -0
  82. coco_core/chat_history.py +267 -0
  83. coco_core/chat_loaders.py +26 -0
  84. coco_core/chat_sessions.py +19 -0
  85. coco_core/cross_encoders.py +18 -0
  86. coco_core/document_loaders/__init__.py +36 -0
  87. coco_core/document_loaders/base.py +155 -0
  88. coco_core/document_loaders/blob_loaders.py +38 -0
  89. coco_core/documents/__init__.py +55 -0
  90. coco_core/documents/base.py +347 -0
  91. coco_core/documents/compressor.py +74 -0
  92. coco_core/documents/transformers.py +79 -0
  93. coco_core/embeddings/__init__.py +31 -0
  94. coco_core/embeddings/embeddings.py +78 -0
  95. coco_core/embeddings/fake.py +129 -0
  96. coco_core/env.py +22 -0
  97. coco_core/example_selectors/__init__.py +47 -0
  98. coco_core/example_selectors/base.py +60 -0
  99. coco_core/example_selectors/length_based.py +131 -0
  100. coco_core/example_selectors/semantic_similarity.py +366 -0
  101. coco_core/exceptions.py +166 -0
  102. coco_core/globals.py +72 -0
  103. coco_core/indexing/__init__.py +53 -0
  104. coco_core/indexing/api.py +954 -0
  105. coco_core/indexing/base.py +661 -0
  106. coco_core/indexing/in_memory.py +104 -0
  107. coco_core/language_models/__init__.py +116 -0
  108. coco_core/language_models/_compat_bridge.py +844 -0
  109. coco_core/language_models/_utils.py +343 -0
  110. coco_core/language_models/base.py +500 -0
  111. coco_core/language_models/chat_model_stream.py +1525 -0
  112. coco_core/language_models/chat_models.py +2748 -0
  113. coco_core/language_models/fake.py +137 -0
  114. coco_core/language_models/fake_chat_models.py +396 -0
  115. coco_core/language_models/llms.py +1569 -0
  116. coco_core/language_models/model_profile.py +183 -0
  117. coco_core/load/__init__.py +44 -0
  118. coco_core/load/_validation.py +191 -0
  119. coco_core/load/dump.py +120 -0
  120. coco_core/load/load.py +828 -0
  121. coco_core/load/mapping.py +1097 -0
  122. coco_core/load/serializable.py +406 -0
  123. coco_core/messages/__init__.py +198 -0
  124. coco_core/messages/ai.py +854 -0
  125. coco_core/messages/base.py +518 -0
  126. coco_core/messages/block_translators/__init__.py +112 -0
  127. coco_core/messages/block_translators/anthropic.py +534 -0
  128. coco_core/messages/block_translators/bedrock.py +108 -0
  129. coco_core/messages/block_translators/bedrock_converse.py +329 -0
  130. coco_core/messages/block_translators/coco_v0.py +303 -0
  131. coco_core/messages/block_translators/google_genai.py +610 -0
  132. coco_core/messages/block_translators/google_vertexai.py +21 -0
  133. coco_core/messages/block_translators/groq.py +157 -0
  134. coco_core/messages/block_translators/openai.py +1089 -0
  135. coco_core/messages/chat.py +64 -0
  136. coco_core/messages/content.py +1488 -0
  137. coco_core/messages/function.py +62 -0
  138. coco_core/messages/human.py +70 -0
  139. coco_core/messages/modifier.py +33 -0
  140. coco_core/messages/system.py +70 -0
  141. coco_core/messages/tool.py +418 -0
  142. coco_core/messages/utils.py +2442 -0
  143. coco_core/output_parsers/__init__.py +101 -0
  144. coco_core/output_parsers/base.py +361 -0
  145. coco_core/output_parsers/format_instructions.py +16 -0
  146. coco_core/output_parsers/json.py +139 -0
  147. coco_core/output_parsers/list.py +249 -0
  148. coco_core/output_parsers/openai_functions.py +324 -0
  149. coco_core/output_parsers/openai_tools.py +383 -0
  150. coco_core/output_parsers/pydantic.py +141 -0
  151. coco_core/output_parsers/string.py +63 -0
  152. coco_core/output_parsers/transform.py +175 -0
  153. coco_core/output_parsers/xml.py +301 -0
  154. coco_core/outputs/__init__.py +66 -0
  155. coco_core/outputs/chat_generation.py +147 -0
  156. coco_core/outputs/chat_result.py +39 -0
  157. coco_core/outputs/generation.py +80 -0
  158. coco_core/outputs/llm_result.py +112 -0
  159. coco_core/outputs/run_info.py +22 -0
  160. coco_core/prompt_values.py +161 -0
  161. coco_core/prompts/__init__.py +101 -0
  162. coco_core/prompts/base.py +508 -0
  163. coco_core/prompts/chat.py +1495 -0
  164. coco_core/prompts/dict.py +177 -0
  165. coco_core/prompts/few_shot.py +483 -0
  166. coco_core/prompts/few_shot_with_templates.py +239 -0
  167. coco_core/prompts/image.py +177 -0
  168. coco_core/prompts/loading.py +296 -0
  169. coco_core/prompts/message.py +97 -0
  170. coco_core/prompts/prompt.py +312 -0
  171. coco_core/prompts/string.py +399 -0
  172. coco_core/prompts/structured.py +214 -0
  173. coco_core/py.typed +0 -0
  174. coco_core/rate_limiters.py +254 -0
  175. coco_core/retrievers.py +328 -0
  176. coco_core/runnables/__init__.py +136 -0
  177. coco_core/runnables/base.py +6725 -0
  178. coco_core/runnables/branch.py +457 -0
  179. coco_core/runnables/config.py +712 -0
  180. coco_core/runnables/configurable.py +710 -0
  181. coco_core/runnables/fallbacks.py +663 -0
  182. coco_core/runnables/graph.py +741 -0
  183. coco_core/runnables/graph_ascii.py +366 -0
  184. coco_core/runnables/graph_mermaid.py +503 -0
  185. coco_core/runnables/graph_png.py +215 -0
  186. coco_core/runnables/history.py +631 -0
  187. coco_core/runnables/passthrough.py +849 -0
  188. coco_core/runnables/retry.py +379 -0
  189. coco_core/runnables/router.py +239 -0
  190. coco_core/runnables/schema.py +188 -0
  191. coco_core/runnables/utils.py +798 -0
  192. coco_core/stores.py +291 -0
  193. coco_core/structured_query.py +203 -0
  194. coco_core/sys_info.py +154 -0
  195. coco_core/tools/__init__.py +95 -0
  196. coco_core/tools/base.py +1948 -0
  197. coco_core/tools/convert.py +497 -0
  198. coco_core/tools/render.py +67 -0
  199. coco_core/tools/retriever.py +94 -0
  200. coco_core/tools/simple.py +208 -0
  201. coco_core/tools/structured.py +322 -0
  202. coco_core/tracers/__init__.py +47 -0
  203. coco_core/tracers/_compat.py +95 -0
  204. coco_core/tracers/_context.py +337 -0
  205. coco_core/tracers/_streaming.py +53 -0
  206. coco_core/tracers/base.py +955 -0
  207. coco_core/tracers/coco.py +279 -0
  208. coco_core/tracers/context.py +205 -0
  209. coco_core/tracers/core.py +766 -0
  210. coco_core/tracers/event_stream.py +1101 -0
  211. coco_core/tracers/log_stream.py +765 -0
  212. coco_core/tracers/memory_stream.py +200 -0
  213. coco_core/tracers/root_listeners.py +130 -0
  214. coco_core/tracers/run_collector.py +40 -0
  215. coco_core/tracers/schemas.py +14 -0
  216. coco_core/tracers/stdout.py +205 -0
  217. coco_core/utils/__init__.py +111 -0
  218. coco_core/utils/_gateway.py +274 -0
  219. coco_core/utils/_merge.py +218 -0
  220. coco_core/utils/aiter.py +357 -0
  221. coco_core/utils/env.py +86 -0
  222. coco_core/utils/formatting.py +80 -0
  223. coco_core/utils/function_calling.py +869 -0
  224. coco_core/utils/html.py +132 -0
  225. coco_core/utils/image.py +15 -0
  226. coco_core/utils/input.py +82 -0
  227. coco_core/utils/interactive_env.py +12 -0
  228. coco_core/utils/iter.py +229 -0
  229. coco_core/utils/json.py +230 -0
  230. coco_core/utils/json_schema.py +273 -0
  231. coco_core/utils/mustache.py +706 -0
  232. coco_core/utils/pydantic.py +631 -0
  233. coco_core/utils/strings.py +69 -0
  234. coco_core/utils/usage.py +61 -0
  235. coco_core/utils/utils.py +528 -0
  236. coco_core/utils/uuid.py +57 -0
  237. coco_core/vectorstores/__init__.py +53 -0
  238. coco_core/vectorstores/base.py +1111 -0
  239. coco_core/vectorstores/in_memory.py +546 -0
  240. coco_core/vectorstores/utils.py +163 -0
  241. coco_core/version.py +3 -0
  242. coco_framework-0.1.0.dist-info/METADATA +184 -0
  243. coco_framework-0.1.0.dist-info/RECORD +400 -0
  244. coco_framework-0.1.0.dist-info/WHEEL +4 -0
  245. coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
  246. coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
  247. coco_graph/_internal/__init__.py +4 -0
  248. coco_graph/_internal/_cache.py +31 -0
  249. coco_graph/_internal/_config.py +474 -0
  250. coco_graph/_internal/_constants.py +140 -0
  251. coco_graph/_internal/_fields.py +213 -0
  252. coco_graph/_internal/_future.py +220 -0
  253. coco_graph/_internal/_pydantic.py +275 -0
  254. coco_graph/_internal/_queue.py +124 -0
  255. coco_graph/_internal/_replay.py +90 -0
  256. coco_graph/_internal/_retry.py +29 -0
  257. coco_graph/_internal/_runnable.py +969 -0
  258. coco_graph/_internal/_scratchpad.py +19 -0
  259. coco_graph/_internal/_serde.py +253 -0
  260. coco_graph/_internal/_timeout.py +25 -0
  261. coco_graph/_internal/_typing.py +54 -0
  262. coco_graph/cache/base/__init__.py +48 -0
  263. coco_graph/cache/base/py.typed +0 -0
  264. coco_graph/cache/memory/__init__.py +73 -0
  265. coco_graph/cache/redis/__init__.py +144 -0
  266. coco_graph/cache/sqlite/__init__.py +120 -0
  267. coco_graph/callbacks.py +394 -0
  268. coco_graph/channels/__init__.py +29 -0
  269. coco_graph/channels/any_value.py +72 -0
  270. coco_graph/channels/base.py +121 -0
  271. coco_graph/channels/binop.py +155 -0
  272. coco_graph/channels/delta.py +202 -0
  273. coco_graph/channels/ephemeral_value.py +79 -0
  274. coco_graph/channels/last_value.py +151 -0
  275. coco_graph/channels/named_barrier_value.py +167 -0
  276. coco_graph/channels/topic.py +94 -0
  277. coco_graph/channels/untracked_value.py +73 -0
  278. coco_graph/checkpoint/base/__init__.py +861 -0
  279. coco_graph/checkpoint/base/id.py +109 -0
  280. coco_graph/checkpoint/base/py.typed +0 -0
  281. coco_graph/checkpoint/memory/__init__.py +698 -0
  282. coco_graph/checkpoint/memory/py.typed +0 -0
  283. coco_graph/checkpoint/postgres/__init__.py +602 -0
  284. coco_graph/checkpoint/postgres/_ainternal.py +23 -0
  285. coco_graph/checkpoint/postgres/_internal.py +21 -0
  286. coco_graph/checkpoint/postgres/aio.py +691 -0
  287. coco_graph/checkpoint/postgres/base.py +666 -0
  288. coco_graph/checkpoint/postgres/py.typed +0 -0
  289. coco_graph/checkpoint/postgres/shallow.py +967 -0
  290. coco_graph/checkpoint/serde/__init__.py +0 -0
  291. coco_graph/checkpoint/serde/_msgpack.py +97 -0
  292. coco_graph/checkpoint/serde/base.py +64 -0
  293. coco_graph/checkpoint/serde/encrypted.py +80 -0
  294. coco_graph/checkpoint/serde/event_hooks.py +52 -0
  295. coco_graph/checkpoint/serde/jsonplus.py +883 -0
  296. coco_graph/checkpoint/serde/py.typed +0 -0
  297. coco_graph/checkpoint/serde/types.py +68 -0
  298. coco_graph/checkpoint/sqlite/__init__.py +645 -0
  299. coco_graph/checkpoint/sqlite/_delta.py +172 -0
  300. coco_graph/checkpoint/sqlite/aio.py +738 -0
  301. coco_graph/checkpoint/sqlite/py.typed +0 -0
  302. coco_graph/checkpoint/sqlite/utils.py +116 -0
  303. coco_graph/config.py +196 -0
  304. coco_graph/constants.py +64 -0
  305. coco_graph/errors.py +241 -0
  306. coco_graph/func/__init__.py +620 -0
  307. coco_graph/graph/__init__.py +12 -0
  308. coco_graph/graph/_branch.py +225 -0
  309. coco_graph/graph/_node.py +103 -0
  310. coco_graph/graph/message.py +437 -0
  311. coco_graph/graph/state.py +1978 -0
  312. coco_graph/graph/ui.py +227 -0
  313. coco_graph/managed/__init__.py +3 -0
  314. coco_graph/managed/base.py +31 -0
  315. coco_graph/managed/is_last_step.py +24 -0
  316. coco_graph/prebuilt/__init__.py +23 -0
  317. coco_graph/prebuilt/_tool_call_stream.py +117 -0
  318. coco_graph/prebuilt/_tool_call_transformer.py +165 -0
  319. coco_graph/prebuilt/chat_agent_executor.py +1015 -0
  320. coco_graph/prebuilt/interrupt.py +105 -0
  321. coco_graph/prebuilt/py.typed +0 -0
  322. coco_graph/prebuilt/tool_node.py +2030 -0
  323. coco_graph/prebuilt/tool_validator.py +221 -0
  324. coco_graph/pregel/__init__.py +3 -0
  325. coco_graph/pregel/_algo.py +1460 -0
  326. coco_graph/pregel/_call.py +298 -0
  327. coco_graph/pregel/_checkpoint.py +331 -0
  328. coco_graph/pregel/_config.py +0 -0
  329. coco_graph/pregel/_draw.py +294 -0
  330. coco_graph/pregel/_executor.py +223 -0
  331. coco_graph/pregel/_io.py +174 -0
  332. coco_graph/pregel/_log.py +3 -0
  333. coco_graph/pregel/_loop.py +1988 -0
  334. coco_graph/pregel/_messages.py +461 -0
  335. coco_graph/pregel/_read.py +312 -0
  336. coco_graph/pregel/_retry.py +854 -0
  337. coco_graph/pregel/_runner.py +941 -0
  338. coco_graph/pregel/_tools.py +268 -0
  339. coco_graph/pregel/_utils.py +221 -0
  340. coco_graph/pregel/_validate.py +120 -0
  341. coco_graph/pregel/_write.py +192 -0
  342. coco_graph/pregel/debug.py +302 -0
  343. coco_graph/pregel/main.py +4364 -0
  344. coco_graph/pregel/protocol.py +288 -0
  345. coco_graph/pregel/types.py +38 -0
  346. coco_graph/py.typed +0 -0
  347. coco_graph/runtime.py +350 -0
  348. coco_graph/store/base/__init__.py +1322 -0
  349. coco_graph/store/base/batch.py +371 -0
  350. coco_graph/store/base/embed.py +436 -0
  351. coco_graph/store/base/py.typed +0 -0
  352. coco_graph/store/memory/__init__.py +592 -0
  353. coco_graph/store/memory/py.typed +0 -0
  354. coco_graph/store/postgres/__init__.py +4 -0
  355. coco_graph/store/postgres/aio.py +592 -0
  356. coco_graph/store/postgres/base.py +1480 -0
  357. coco_graph/store/postgres/py.typed +0 -0
  358. coco_graph/store/sqlite/__init__.py +4 -0
  359. coco_graph/store/sqlite/aio.py +632 -0
  360. coco_graph/store/sqlite/base.py +1514 -0
  361. coco_graph/stream/__init__.py +45 -0
  362. coco_graph/stream/_convert.py +32 -0
  363. coco_graph/stream/_mux.py +523 -0
  364. coco_graph/stream/_types.py +330 -0
  365. coco_graph/stream/run_stream.py +735 -0
  366. coco_graph/stream/stream_channel.py +341 -0
  367. coco_graph/stream/transformers.py +1039 -0
  368. coco_graph/types.py +1079 -0
  369. coco_graph/typing.py +48 -0
  370. coco_graph/utils/__init__.py +1 -0
  371. coco_graph/utils/config.py +4 -0
  372. coco_graph/utils/runnable.py +3 -0
  373. coco_graph/version.py +13 -0
  374. coco_graph/warnings.py +69 -0
  375. coco_openai/__init__.py +20 -0
  376. coco_openai/_compat.py +45 -0
  377. coco_openai/_version.py +3 -0
  378. coco_openai/chat_models/__init__.py +6 -0
  379. coco_openai/chat_models/_client_utils.py +684 -0
  380. coco_openai/chat_models/_compat.py +525 -0
  381. coco_openai/chat_models/azure.py +1227 -0
  382. coco_openai/chat_models/base.py +5654 -0
  383. coco_openai/chat_models/codex.py +557 -0
  384. coco_openai/chatgpt_oauth.py +1063 -0
  385. coco_openai/data/__init__.py +1 -0
  386. coco_openai/data/_profiles.py +1655 -0
  387. coco_openai/data/profile_augmentations.toml +127 -0
  388. coco_openai/embeddings/__init__.py +6 -0
  389. coco_openai/embeddings/azure.py +240 -0
  390. coco_openai/embeddings/base.py +820 -0
  391. coco_openai/llms/__init__.py +6 -0
  392. coco_openai/llms/azure.py +249 -0
  393. coco_openai/llms/base.py +919 -0
  394. coco_openai/middleware/__init__.py +11 -0
  395. coco_openai/middleware/openai_moderation.py +484 -0
  396. coco_openai/output_parsers/__init__.py +9 -0
  397. coco_openai/output_parsers/tools.py +9 -0
  398. coco_openai/py.typed +0 -0
  399. coco_openai/tools/__init__.py +5 -0
  400. 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