timbal 2.1.7__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. {timbal-2.1.7 → timbal-2.2.0}/PKG-INFO +1 -1
  2. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/_version.py +2 -2
  3. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/tool.py +45 -0
  4. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/errors.py +51 -1
  5. timbal-2.2.0/python/timbal/platform/tool_proxy.py +98 -0
  6. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/platform/utils.py +8 -4
  7. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/config.py +2 -0
  8. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/config_loader.py +15 -1
  9. timbal-2.2.0/python/timbal/tools/_creds.py +82 -0
  10. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/cala.py +13 -14
  11. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/elevenlabs.py +12 -25
  12. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/fal.py +5 -23
  13. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/firecrawl.py +31 -16
  14. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/gemini_images.py +5 -20
  15. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/google_maps.py +5 -20
  16. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/happy_scribe.py +7 -12
  17. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/klaviyo.py +36 -52
  18. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/krea.py +6 -23
  19. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/linkedin.py +5 -21
  20. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/mongodb.py +49 -30
  21. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/pinecone.py +8 -24
  22. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/powerbi.py +7 -23
  23. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/quiver_ai.py +5 -21
  24. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/replicate.py +29 -23
  25. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/resend.py +73 -24
  26. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/salesforce.py +169 -41
  27. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/scraperapi.py +31 -16
  28. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/slack.py +190 -43
  29. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/stripe.py +51 -67
  30. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/tavily.py +25 -15
  31. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/web_search.py +25 -8
  32. timbal-2.1.7/python/timbal/tools/_creds.py +0 -39
  33. {timbal-2.1.7 → timbal-2.2.0}/.gitignore +0 -0
  34. {timbal-2.1.7 → timbal-2.2.0}/LICENSE +0 -0
  35. {timbal-2.1.7 → timbal-2.2.0}/README.md +0 -0
  36. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/chat-completions.mdx +0 -0
  37. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/create-from-workforce.mdx +0 -0
  38. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/jobs/cancel.mdx +0 -0
  39. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/jobs/get.mdx +0 -0
  40. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/jobs/list.mdx +0 -0
  41. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/jobs/retry.mdx +0 -0
  42. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/link.mdx +0 -0
  43. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/list-context-vars.mdx +0 -0
  44. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/list-policies.mdx +0 -0
  45. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/messages.mdx +0 -0
  46. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/responses.mdx +0 -0
  47. {timbal-2.1.7 → timbal-2.2.0}/docs/api-reference/ace/unlink.mdx +0 -0
  48. {timbal-2.1.7 → timbal-2.2.0}/pyproject.toml +0 -0
  49. {timbal-2.1.7 → timbal-2.2.0}/pyrightconfig.json +0 -0
  50. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/__init__.py +0 -0
  51. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/__init__.py +0 -0
  52. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/__main__.py +0 -0
  53. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/cli_utils.py +0 -0
  54. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/cst_utils.py +0 -0
  55. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/flow.py +0 -0
  56. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/format.py +0 -0
  57. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/model_discovery.py +0 -0
  58. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/test.py +0 -0
  59. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/tool_discovery.py +0 -0
  60. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/__init__.py +0 -0
  61. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/add_edge.py +0 -0
  62. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/add_step.py +0 -0
  63. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/add_tool.py +0 -0
  64. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/remove_edge.py +0 -0
  65. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/remove_step.py +0 -0
  66. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/remove_tool.py +0 -0
  67. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/set_config.py +0 -0
  68. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/set_param.py +0 -0
  69. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/codegen/transformers/set_position.py +0 -0
  70. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/__init__.py +0 -0
  71. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/base.py +0 -0
  72. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/impl/__init__.py +0 -0
  73. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/impl/anthropic.py +0 -0
  74. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/impl/default.py +0 -0
  75. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/impl/message.py +0 -0
  76. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/impl/openai.py +0 -0
  77. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/impl/string.py +0 -0
  78. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/impl/timbal.py +0 -0
  79. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/collectors/registry.py +0 -0
  80. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/__init__.py +0 -0
  81. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/agent.py +0 -0
  82. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/fallback_model.py +0 -0
  83. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/llm_router.py +0 -0
  84. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/mcp.py +0 -0
  85. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/memory_compaction.py +0 -0
  86. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/models.py +0 -0
  87. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/runnable.py +0 -0
  88. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/skill.py +0 -0
  89. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/test_model.py +0 -0
  90. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/tool_set.py +0 -0
  91. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/core/workflow.py +0 -0
  92. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/__init__.py +0 -0
  93. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/cli.py +0 -0
  94. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/display.py +0 -0
  95. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/fixtures/agent.py +0 -0
  96. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/fixtures/eval_examples.yaml +0 -0
  97. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/fixtures/eval_flow_validators.yaml +0 -0
  98. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/fixtures/eval_negations.yaml +0 -0
  99. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/fixtures/eval_parallel.yaml +0 -0
  100. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/fixtures/eval_subagent.yaml +0 -0
  101. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/fixtures/eval_transforms.yaml +0 -0
  102. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/fixtures/test_invalid_validator.yaml +0 -0
  103. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/models.py +0 -0
  104. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/runner.py +0 -0
  105. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/utils.py +0 -0
  106. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/__init__.py +0 -0
  107. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/base.py +0 -0
  108. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/comparison_base.py +0 -0
  109. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/contains.py +0 -0
  110. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/contains_all.py +0 -0
  111. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/contains_any.py +0 -0
  112. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/context.py +0 -0
  113. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/email.py +0 -0
  114. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/ends_with.py +0 -0
  115. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/eq.py +0 -0
  116. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/gt.py +0 -0
  117. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/gte.py +0 -0
  118. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/json.py +0 -0
  119. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/language.py +0 -0
  120. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/length.py +0 -0
  121. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/llm_base.py +0 -0
  122. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/lt.py +0 -0
  123. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/lte.py +0 -0
  124. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/max_length.py +0 -0
  125. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/min_length.py +0 -0
  126. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/not_null.py +0 -0
  127. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/parallel.py +0 -0
  128. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/pattern.py +0 -0
  129. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/prompt.py +0 -0
  130. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/semantic.py +0 -0
  131. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/seq.py +0 -0
  132. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/starts_with.py +0 -0
  133. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/evals/validators/type.py +0 -0
  134. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/logs.py +0 -0
  135. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/models.yaml +0 -0
  136. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/platform/__init__.py +0 -0
  137. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/platform/integrations.py +0 -0
  138. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/platform/knowledge_bases/__init__.py +0 -0
  139. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/platform/knowledge_bases/query.py +0 -0
  140. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/platform/types.py +0 -0
  141. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/server/__init__.py +0 -0
  142. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/server/__main__.py +0 -0
  143. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/server/http.py +0 -0
  144. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/server/jobs.py +0 -0
  145. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/server/voice.html +0 -0
  146. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/server/voice.py +0 -0
  147. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/__init__.py +0 -0
  148. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/context.py +0 -0
  149. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/dependency_analyzer.py +0 -0
  150. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/__init__.py +0 -0
  151. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/exporters/__init__.py +0 -0
  152. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/exporters/otel.py +0 -0
  153. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/providers/__init__.py +0 -0
  154. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/providers/base.py +0 -0
  155. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/providers/in_memory.py +0 -0
  156. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/providers/jsonl.py +0 -0
  157. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/providers/platform.py +0 -0
  158. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/providers/sqlite.py +0 -0
  159. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/span.py +0 -0
  160. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/state/tracing/trace.py +0 -0
  161. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/__init__.py +0 -0
  162. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/asana.py +0 -0
  163. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/bash.py +0 -0
  164. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/cloudflare.py +0 -0
  165. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/dynamics_business_central.py +0 -0
  166. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/dynamics_sales.py +0 -0
  167. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/edit.py +0 -0
  168. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/elasticsearch.py +0 -0
  169. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/excel.py +0 -0
  170. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/fathom.py +0 -0
  171. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/gmail.py +0 -0
  172. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/google_analytics.py +0 -0
  173. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/google_business.py +0 -0
  174. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/google_calendar.py +0 -0
  175. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/google_docs.py +0 -0
  176. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/google_drive.py +0 -0
  177. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/google_merchant_center.py +0 -0
  178. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/google_search_console.py +0 -0
  179. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/google_sheets.py +0 -0
  180. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/hubspot.py +0 -0
  181. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/interaction.py +0 -0
  182. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/jira.py +0 -0
  183. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/knowledge_base.py +0 -0
  184. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/lancedb.py +0 -0
  185. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/netsuite.py +0 -0
  186. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/onedrive.py +0 -0
  187. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/outlook.py +0 -0
  188. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/read.py +0 -0
  189. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/sharepoint.py +0 -0
  190. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/shopify.py +0 -0
  191. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/write.py +0 -0
  192. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/xai.py +0 -0
  193. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/zendesk.py +0 -0
  194. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/tools/zoho_crm.py +0 -0
  195. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/__init__.py +0 -0
  196. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/approval.py +0 -0
  197. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/content/__init__.py +0 -0
  198. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/content/base.py +0 -0
  199. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/content/custom.py +0 -0
  200. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/content/file.py +0 -0
  201. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/content/text.py +0 -0
  202. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/content/thinking.py +0 -0
  203. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/content/tool_result.py +0 -0
  204. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/content/tool_use.py +0 -0
  205. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/events/__init__.py +0 -0
  206. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/events/approval.py +0 -0
  207. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/events/base.py +0 -0
  208. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/events/delta.py +0 -0
  209. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/events/interaction.py +0 -0
  210. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/events/output.py +0 -0
  211. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/events/start.py +0 -0
  212. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/file.py +0 -0
  213. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/message.py +0 -0
  214. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/types/run_status.py +0 -0
  215. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/utils/__init__.py +0 -0
  216. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/utils/import_spec.py +0 -0
  217. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/utils/model.py +0 -0
  218. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/utils/net.py +0 -0
  219. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/utils/schema.py +0 -0
  220. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/utils/serialization.py +0 -0
  221. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/voice/__init__.py +0 -0
  222. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/voice/elevenlabs.py +0 -0
  223. {timbal-2.1.7 → timbal-2.2.0}/python/timbal/voice/session.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: timbal
3
- Version: 2.1.7
3
+ Version: 2.2.0
4
4
  Summary: Simple, performant, battle-tested framework for building reliable AI applications
5
5
  Project-URL: Source, https://github.com/timbal-ai/timbal
6
6
  Author-email: Timbal <team@timbal.ai>
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.1.7'
22
- __version_tuple__ = version_tuple = (2, 1, 7)
21
+ __version__ = version = '2.2.0'
22
+ __version_tuple__ = version_tuple = (2, 2, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -11,6 +11,8 @@ except ImportError:
11
11
 
12
12
  from pydantic import BaseModel, Field, SkipValidation, computed_field, model_validator
13
13
 
14
+ from ..errors import CredentialNotAvailable, PlatformError, ToolProxyUnavailable
15
+ from ..platform.tool_proxy import execute_tool_proxy
14
16
  from ..utils import create_model_from_handler
15
17
  from .runnable import Runnable
16
18
 
@@ -119,6 +121,49 @@ class Tool(Runnable):
119
121
  self._is_async_gen = inspect_result["is_async_gen"]
120
122
  self._dependencies = inspect_result["dependencies"]
121
123
 
124
+ @override
125
+ async def _execute_handler(
126
+ self,
127
+ validated_input: dict[str, Any],
128
+ run_context: Any,
129
+ span: Any,
130
+ event_queue: Any = None,
131
+ ):
132
+ """Execute handler locally; on missing credentials, run via platform tool proxy.
133
+
134
+ If no proxy is reachable for this tool — the platform has no proxy for it
135
+ (HTTP 403, or 404/501 fallbacks), or there's no platform config at all
136
+ (e.g. running locally) — re-raise the original ``CredentialNotAvailable``
137
+ so the user gets the actionable "configure credentials locally" message
138
+ instead of an opaque platform error.
139
+
140
+ When platform auth is configured but org/subject is missing (e.g.
141
+ ``TIMBAL_API_KEY`` without ``TIMBAL_ORG_ID``), propagate
142
+ ``ToolProxyUnavailable`` so the user fixes platform setup rather than
143
+ local provider credentials.
144
+ """
145
+ try:
146
+ async for event, final_output, collector in super()._execute_handler(
147
+ validated_input, run_context, span, event_queue
148
+ ):
149
+ yield (event, final_output, collector)
150
+ except CredentialNotAvailable as cred_error:
151
+ try:
152
+ output = await execute_tool_proxy(self.name, validated_input)
153
+ except ToolProxyUnavailable:
154
+ if run_context.platform_config is None:
155
+ # Local run without platform config — fall back to credential error.
156
+ raise cred_error from None
157
+ raise
158
+ except PlatformError as proxy_error:
159
+ # 403 is what the platform returns when no proxy is available for this
160
+ # tool (no service-account credentials configured). 404/501 are kept as
161
+ # fallbacks. In all these cases, surface the actionable credential error.
162
+ if proxy_error.status_code in (403, 404, 501):
163
+ raise cred_error from None
164
+ raise
165
+ yield (None, output, None)
166
+
122
167
  @override
123
168
  def nest(self, parent_path: str) -> None:
124
169
  """See base class."""
@@ -9,6 +9,38 @@ class APIKeyNotFoundError(TimbalError):
9
9
  """Error raised when an API key is not found."""
10
10
 
11
11
 
12
+ class CredentialNotAvailable(APIKeyNotFoundError, ValueError):
13
+ """Raised when a tool credential can't be resolved from any source.
14
+
15
+ Subclasses ValueError for backward compat with tool code/tests. Carries
16
+ structured fields (provider_name, missing, env_vars) so callers (e.g. a
17
+ remote service-account executor) can react without parsing the message.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ provider_name: str,
23
+ *,
24
+ missing: list[str] | None = None,
25
+ env_vars: list[str] | None = None,
26
+ message: str | None = None,
27
+ ) -> None:
28
+ self.provider_name = provider_name
29
+ self.missing = list(missing or [])
30
+ self.env_vars = list(env_vars or [])
31
+ super().__init__(message or self._default_message())
32
+
33
+ def _default_message(self) -> str:
34
+ parts = [f"{self.provider_name} credentials not found."]
35
+ if self.missing:
36
+ parts.append(f"Missing: {', '.join(self.missing)}.")
37
+ hints = ["pass them on the tool", "configure an Integration"]
38
+ if self.env_vars:
39
+ hints.append(f"set the {', '.join(self.env_vars)} environment variable(s)")
40
+ parts.append("To fix: " + ", or ".join(hints) + ".")
41
+ return " ".join(parts)
42
+
43
+
12
44
  class EarlyExit(TimbalError):
13
45
  """Error raised when an early exit is requested.
14
46
 
@@ -133,7 +165,25 @@ class PDFProcessingError(TimbalError):
133
165
 
134
166
 
135
167
  class PlatformError(TimbalError):
136
- """Error raised when a platform API call fails."""
168
+ """Error raised when a platform API call fails.
169
+
170
+ Carries the HTTP ``status_code`` (when the failure came from an HTTP
171
+ response) so callers can branch on it — e.g. treat 404/501 as "endpoint
172
+ not implemented" rather than a hard failure.
173
+ """
174
+
175
+ def __init__(self, message: str, *, status_code: int | None = None) -> None:
176
+ super().__init__(message)
177
+ self.status_code = status_code
178
+
179
+
180
+ class ToolProxyUnavailable(TimbalError):
181
+ """Raised when the platform tool proxy can't be used for a tool execution.
182
+
183
+ Signals that no proxy is reachable for this run — e.g. no platform config
184
+ (running locally without TIMBAL_API_KEY / TIMBAL_ORG_ID). Callers fall back
185
+ to the original credential error so the user gets an actionable message.
186
+ """
137
187
 
138
188
 
139
189
  class FallbackExhausted(TimbalError):
@@ -0,0 +1,98 @@
1
+ """Platform tool proxy — remote tool execution without exporting credentials."""
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import structlog
7
+
8
+ from .. import __version__
9
+ from ..errors import ToolProxyUnavailable
10
+ from ..state import get_call_id, get_or_create_run_context
11
+ from .utils import _request
12
+
13
+ logger = structlog.get_logger("timbal.platform.tool_proxy")
14
+
15
+ _PROXY_PREFIX = "/proxies/v1/tools"
16
+
17
+
18
+ def _subject_header_env_map() -> dict[str, str]:
19
+ """Map x-timbal-* header names to TIMBAL_* env vars (fallback when subject fields are unset)."""
20
+ return {
21
+ "x-timbal-app-id": "TIMBAL_APP_ID",
22
+ "x-timbal-project-id": "TIMBAL_PROJECT_ID",
23
+ "x-timbal-rev": "TIMBAL_REV",
24
+ }
25
+
26
+
27
+ def build_tool_proxy_headers() -> dict[str, str]:
28
+ """Build per-request headers for tool proxy calls (run context + subject + version)."""
29
+ run_context = get_or_create_run_context()
30
+ headers: dict[str, str] = {
31
+ "x-timbal-run-id": run_context.id,
32
+ "x-timbal-version": __version__,
33
+ }
34
+
35
+ # Match llm_router: always attach call_id when the context var is set (tool runs always set it).
36
+ call_id = get_call_id()
37
+ if call_id is not None:
38
+ headers["x-timbal-call-id"] = call_id
39
+
40
+ platform_config = run_context.platform_config
41
+ if platform_config and platform_config.subject:
42
+ subject = platform_config.subject
43
+ if subject.app_id:
44
+ headers["x-timbal-app-id"] = subject.app_id
45
+ if subject.project_id:
46
+ headers["x-timbal-project-id"] = subject.project_id
47
+ if subject.rev:
48
+ headers["x-timbal-rev"] = subject.rev
49
+
50
+ for header_name, env_var in _subject_header_env_map().items():
51
+ if header_name not in headers:
52
+ value = os.getenv(env_var)
53
+ if value:
54
+ headers[header_name] = value
55
+
56
+ return headers
57
+
58
+
59
+ def _tool_proxy_path(org_id: str, tool_name: str) -> str:
60
+ return f"orgs/{org_id}{_PROXY_PREFIX}/{tool_name}"
61
+
62
+
63
+ async def execute_tool_proxy(tool_name: str, params: dict[str, Any]) -> Any:
64
+ """Execute a tool via the platform proxy (no credentials in the client runtime).
65
+
66
+ POST ``/orgs/{org_id}/proxies/v1/tools/{tool_name}`` with handler params as JSON body.
67
+ Returns the tool handler result JSON as-is (no response wrapping).
68
+ """
69
+ run_context = get_or_create_run_context()
70
+ platform_config = run_context.platform_config
71
+ if platform_config is None or platform_config.subject is None:
72
+ # The run context may have skipped platform_config resolution (e.g. tracing was
73
+ # explicitly disabled). Resolve it on demand — the proxy needs the org subject.
74
+ # force_refresh bypasses a stale cached default (a default call could have cached
75
+ # None before TIMBAL_API_KEY / TIMBAL_ORG_ID were set in this process).
76
+ from ..state.config_loader import resolve_platform_config
77
+
78
+ platform_config = resolve_platform_config(platform_config, force_refresh=True)
79
+ run_context.platform_config = platform_config
80
+
81
+ if platform_config is None or platform_config.subject is None:
82
+ raise ToolProxyUnavailable(
83
+ "Tool proxy requires platform_config with org subject. Set TIMBAL_API_KEY and TIMBAL_ORG_ID."
84
+ )
85
+
86
+ org_id = platform_config.subject.org_id
87
+ path = _tool_proxy_path(org_id, tool_name)
88
+ headers = build_tool_proxy_headers()
89
+
90
+ logger.debug("tool_proxy_execute", tool=tool_name, path=path)
91
+
92
+ response = await _request(
93
+ "POST",
94
+ path,
95
+ headers=headers,
96
+ json=params,
97
+ )
98
+ return response.json()
@@ -134,7 +134,8 @@ async def _request(
134
134
  f"\n"
135
135
  f" URL: {exc.request.url}\n"
136
136
  f" Status: {exc.response.status_code} {exc.response.reason_phrase}\n"
137
- f" Response body: {error_body or None}"
137
+ f" Response body: {error_body or None}",
138
+ status_code=exc.response.status_code,
138
139
  ) from exc
139
140
  # Retry on 429, 5xx, or other errors
140
141
  if attempt == max_retries:
@@ -142,7 +143,8 @@ async def _request(
142
143
  f"\n"
143
144
  f" URL: {exc.request.url}\n"
144
145
  f" Status: {exc.response.status_code} {exc.response.reason_phrase}\n"
145
- f" Response body: {error_body or None}"
146
+ f" Response body: {error_body or None}",
147
+ status_code=exc.response.status_code,
146
148
  ) from exc
147
149
  wait_time = 0.1 * (2**attempt) # Exponential backoff: 100ms, 200ms, 400ms
148
150
  if exc.response.status_code == 429:
@@ -236,7 +238,8 @@ async def _stream(
236
238
  f"\n"
237
239
  f" URL: {exc.request.url}\n"
238
240
  f" Status: {exc.response.status_code} {exc.response.reason_phrase}\n"
239
- f" Response body: {error_body or None}"
241
+ f" Response body: {error_body or None}",
242
+ status_code=exc.response.status_code,
240
243
  ) from exc
241
244
  # Retry on 429, 5xx, or other errors
242
245
  if attempt == max_retries:
@@ -244,7 +247,8 @@ async def _stream(
244
247
  f"\n"
245
248
  f" URL: {exc.request.url}\n"
246
249
  f" Status: {exc.response.status_code} {exc.response.reason_phrase}\n"
247
- f" Response body: {error_body or None}"
250
+ f" Response body: {error_body or None}",
251
+ status_code=exc.response.status_code,
248
252
  ) from exc
249
253
  wait_time = 0.1 * (2**attempt)
250
254
  if exc.response.status_code == 429:
@@ -54,6 +54,8 @@ class PlatformSubject(BaseModel):
54
54
  """Organization identifier."""
55
55
  app_id: str | None = None
56
56
  """Application identifier. Either project or app must be specified."""
57
+ project_id: str | None = None
58
+ """Project identifier (platform project scope)."""
57
59
  rev: str | None = None
58
60
  """Git revision the run was executed against. Internal use."""
59
61
 
@@ -150,6 +150,7 @@ def resolve_platform_config(
150
150
  platform_config: PlatformConfig | None = None,
151
151
  profile: str | None = None,
152
152
  config_dir: Path | None = None,
153
+ force_refresh: bool = False,
153
154
  ) -> PlatformConfig | None:
154
155
  """Resolve platform configuration by merging sources with precedence.
155
156
 
@@ -170,11 +171,14 @@ def resolve_platform_config(
170
171
  platform_config: Existing config to fill in missing fields for.
171
172
  profile: Profile name for file config. Defaults to TIMBAL_PROFILE env var or "default".
172
173
  config_dir: Override the config directory (for testing).
174
+ force_refresh: Re-read env/file sources even on a default call, bypassing
175
+ (and refreshing) the cached default. Use when env credentials may have
176
+ been set after an earlier default resolution cached ``None``.
173
177
  """
174
178
  global _cached_default_config, _default_config_resolved
175
179
 
176
180
  is_default_call = platform_config is None and profile is None and config_dir is None
177
- if is_default_call and _default_config_resolved:
181
+ if is_default_call and _default_config_resolved and not force_refresh:
178
182
  return _cached_default_config.model_copy() if _cached_default_config else None
179
183
 
180
184
  file_config = load_file_config(profile=profile, config_dir=config_dir)
@@ -241,17 +245,27 @@ def resolve_platform_config(
241
245
  if not app_id:
242
246
  app_id = os.getenv("TIMBAL_APP_ID")
243
247
 
248
+ project_id = None
249
+ if existing_subject:
250
+ project_id = existing_subject.project_id
251
+ if not project_id:
252
+ project_id = os.getenv("TIMBAL_PROJECT_ID")
253
+
244
254
  rev = existing_subject.rev if existing_subject else None
255
+ if not rev:
256
+ rev = os.getenv("TIMBAL_REV")
245
257
 
246
258
  platform_config.subject = PlatformSubject(
247
259
  org_id=org_id,
248
260
  app_id=app_id,
261
+ project_id=project_id,
249
262
  rev=rev,
250
263
  )
251
264
  logger.debug(
252
265
  "Resolved platform subject.",
253
266
  org_id=org_id,
254
267
  app_id=app_id,
268
+ project_id=project_id,
255
269
  rev=rev,
256
270
  )
257
271
 
@@ -0,0 +1,82 @@
1
+ """Credential resolution for tools.
2
+
3
+ Per credential, first match wins: explicit value on the tool → Integration
4
+ credentials → env var. Raises CredentialNotAvailable when nothing resolves.
5
+ """
6
+
7
+ import os
8
+ from collections.abc import Mapping, Sequence
9
+ from typing import Any
10
+
11
+ from pydantic import SecretStr
12
+
13
+ from ..errors import CredentialNotAvailable
14
+ from ..platform.integrations import Integration
15
+
16
+ __all__ = [
17
+ "resolve_api_key",
18
+ ]
19
+
20
+
21
+ def _secret_value(value: Any) -> str:
22
+ if isinstance(value, SecretStr):
23
+ return value.get_secret_value()
24
+ return str(value)
25
+
26
+
27
+ def _first_present(credentials: Any, keys: Sequence[str]) -> Any:
28
+ """First truthy value for `keys` in a credentials dict or object."""
29
+ if isinstance(credentials, Mapping):
30
+ for key in keys:
31
+ value = credentials.get(key)
32
+ if value:
33
+ return value
34
+ return None
35
+ for key in keys:
36
+ value = getattr(credentials, key, None)
37
+ if value:
38
+ return value
39
+ return None
40
+
41
+
42
+ async def resolve_api_key(
43
+ *,
44
+ provider_name: str,
45
+ env_var: str | None = None,
46
+ integration: Any = None,
47
+ api_key: SecretStr | str | None = None,
48
+ tool: Any = None,
49
+ explicit_attr: str = "api_key",
50
+ integration_keys: Sequence[str] = ("api_key",),
51
+ ) -> str:
52
+ """Resolve a single provider secret.
53
+
54
+ Pass `tool=self` to pull `integration` / `explicit_attr` off it, or pass
55
+ `integration` / `api_key` explicitly. `integration_keys` are the keys tried
56
+ in the resolved integration credentials.
57
+ """
58
+ if tool is not None:
59
+ if integration is None:
60
+ integration = getattr(tool, "integration", None)
61
+ if api_key is None:
62
+ api_key = getattr(tool, explicit_attr, None)
63
+
64
+ if api_key is not None:
65
+ return _secret_value(api_key)
66
+
67
+ if isinstance(integration, Integration):
68
+ credentials = await integration.resolve()
69
+ value = _first_present(credentials, integration_keys)
70
+ if value is not None:
71
+ return str(value)
72
+
73
+ if env_var:
74
+ env_value = os.getenv(env_var)
75
+ if env_value:
76
+ return env_value
77
+
78
+ raise CredentialNotAvailable(
79
+ provider_name,
80
+ missing=[explicit_attr],
81
+ env_vars=[env_var] if env_var else [],
82
+ )
@@ -4,24 +4,13 @@ from pydantic import Field, SecretStr
4
4
 
5
5
  from ..core.tool import Tool
6
6
  from ..platform.integrations import Integration
7
+ from ._creds import resolve_api_key
7
8
 
8
9
  _DEFAULT_BASE_URL = "https://api.cala.ai/v1"
9
10
  # Back-compat alias for web_search imports
10
11
  _BASE_URL = _DEFAULT_BASE_URL
11
12
 
12
13
 
13
- async def _resolve_api_key(*, integration: Any = None, api_key: SecretStr | None = None) -> str:
14
- """Resolve Cala API key from integration, explicit field, or env var."""
15
- from ._creds import resolve_api_key
16
-
17
- return await resolve_api_key(
18
- env_var="CALA_API_KEY",
19
- provider_name="Cala",
20
- integration=integration,
21
- api_key=api_key,
22
- )
23
-
24
-
25
14
  def _normalize_base_url(base_url: str | None) -> str:
26
15
  return (base_url or _DEFAULT_BASE_URL).rstrip("/")
27
16
 
@@ -106,7 +95,12 @@ class CalaSearch(_CalaKnowledgeTool):
106
95
  explainability: bool = True,
107
96
  return_entities: bool = True,
108
97
  ) -> dict:
109
- api_key = await _resolve_api_key(integration=self.integration, api_key=self.api_key)
98
+ api_key = await resolve_api_key(
99
+ env_var="CALA_API_KEY",
100
+ provider_name="Cala",
101
+ integration=self.integration,
102
+ api_key=self.api_key,
103
+ )
110
104
  return await _post_cala(
111
105
  path="/knowledge/search",
112
106
  api_key=api_key,
@@ -137,7 +131,12 @@ class CalaQuery(_CalaKnowledgeTool):
137
131
  ),
138
132
  return_entities: bool = True,
139
133
  ) -> dict:
140
- api_key = await _resolve_api_key(integration=self.integration, api_key=self.api_key)
134
+ api_key = await resolve_api_key(
135
+ env_var="CALA_API_KEY",
136
+ provider_name="Cala",
137
+ integration=self.integration,
138
+ api_key=self.api_key,
139
+ )
141
140
  return await _post_cala(
142
141
  path="/knowledge/query",
143
142
  api_key=api_key,
@@ -5,24 +5,11 @@ from pydantic import Field, SecretStr
5
5
 
6
6
  from ..core.tool import Tool
7
7
  from ..platform.integrations import Integration
8
+ from ._creds import resolve_api_key
8
9
 
9
10
  _ELEVENLABS_BASE = "https://api.elevenlabs.io/v1"
10
11
 
11
12
 
12
- async def _resolve_api_key(tool: Any) -> str:
13
- if isinstance(tool.integration, Integration):
14
- credentials = await tool.integration.resolve()
15
- return credentials["api_key"]
16
- if tool.api_key is not None:
17
- return tool.api_key.get_secret_value()
18
- env_key = os.getenv("ELEVENLABS_API_KEY")
19
- if env_key:
20
- return env_key
21
- raise ValueError(
22
- "ElevenLabs API key not found. Set ELEVENLABS_API_KEY, pass api_key, or configure an integration."
23
- )
24
-
25
-
26
13
  class ElevenLabsTextToSpeech(Tool):
27
14
  name: str = "elevenlabs_text_to_speech"
28
15
  description: str | None = (
@@ -49,7 +36,7 @@ class ElevenLabsTextToSpeech(Tool):
49
36
  style: float = Field(0.0, description="Style exaggeration (0.0–1.0, only for v2 models)."),
50
37
  use_speaker_boost: bool = Field(True, description="Improve speaker clarity (slight latency increase)."),
51
38
  ) -> Any:
52
- api_key = await _resolve_api_key(self)
39
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
53
40
  import base64
54
41
 
55
42
  import httpx
@@ -99,7 +86,7 @@ class ElevenLabsListPhoneNumbers(Tool):
99
86
 
100
87
  def __init__(self, **kwargs: Any) -> None:
101
88
  async def _list_phone_numbers() -> Any:
102
- api_key = await _resolve_api_key(self)
89
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
103
90
  import httpx
104
91
 
105
92
  async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=10.0)) as client:
@@ -140,7 +127,7 @@ class ElevenLabsMakeOutboundCall(Tool):
140
127
  description="Destination phone number in E.164 format, e.g. '+14155552671'.",
141
128
  ),
142
129
  ) -> Any:
143
- api_key = await _resolve_api_key(self)
130
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
144
131
  import httpx
145
132
 
146
133
  payload: dict[str, Any] = {
@@ -177,7 +164,7 @@ class ElevenLabsListAgents(Tool):
177
164
 
178
165
  def __init__(self, **kwargs: Any) -> None:
179
166
  async def _list_agents() -> Any:
180
- api_key = await _resolve_api_key(self)
167
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
181
168
  import httpx
182
169
 
183
170
  async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=10.0)) as client:
@@ -205,7 +192,7 @@ class ElevenLabsGetModels(Tool):
205
192
 
206
193
  def __init__(self, **kwargs: Any) -> None:
207
194
  async def _get_models() -> Any:
208
- api_key = await _resolve_api_key(self)
195
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
209
196
  import httpx
210
197
 
211
198
  async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=10.0)) as client:
@@ -238,7 +225,7 @@ class ElevenLabsGetVoicesWithDescriptions(Tool):
238
225
  async def _get_voices_with_descriptions(
239
226
  show_legacy: bool = Field(False, description="Include legacy pre-XTTS voices in the response."),
240
227
  ) -> Any:
241
- api_key = await _resolve_api_key(self)
228
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
242
229
  import httpx
243
230
 
244
231
  async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=10.0)) as client:
@@ -292,7 +279,7 @@ class ElevenLabsListHistory(Tool):
292
279
  page_size: int = Field(100, description="Max items to return (default 100)."),
293
280
  source: str | None = Field(None, description="Filter by source: 'TTS', 'STS', or 'ConvAI'."),
294
281
  ) -> Any:
295
- api_key = await _resolve_api_key(self)
282
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
296
283
  import httpx
297
284
 
298
285
  params: dict[str, Any] = {"page_size": page_size}
@@ -329,7 +316,7 @@ class ElevenLabsGetAudioFromHistoryItem(Tool):
329
316
  async def _get_audio_from_history_item(
330
317
  history_item_id: str = Field(..., description="The ID of the history item to retrieve audio for."),
331
318
  ) -> Any:
332
- api_key = await _resolve_api_key(self)
319
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
333
320
  import base64
334
321
 
335
322
  import httpx
@@ -369,7 +356,7 @@ class ElevenLabsDownloadHistoryItems(Tool):
369
356
  async def _download_history_items(
370
357
  history_item_ids: list[str] = Field(..., description="List of history item IDs to download. Single ID returns audio; multiple IDs return a .zip."),
371
358
  ) -> Any:
372
- api_key = await _resolve_api_key(self)
359
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
373
360
  import base64
374
361
  import io
375
362
  import zipfile
@@ -435,7 +422,7 @@ class ElevenLabsCreateAgent(Tool):
435
422
  max_duration_seconds: int = Field(600, description="Maximum call/conversation length in seconds."),
436
423
  turn_timeout: int = Field(20, description="Seconds of silence before the agent ends its turn."),
437
424
  ) -> Any:
438
- api_key = await _resolve_api_key(self)
425
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
439
426
  import httpx
440
427
 
441
428
  payload: dict[str, Any] = {
@@ -505,7 +492,7 @@ class ElevenLabsAddVoice(Tool):
505
492
  description="Optional metadata. Keys: language (BCP-47: 'ca', 'es', 'en', 'fr'), accent, gender, age. E.g. {'language': 'ca', 'accent': 'British'}.",
506
493
  ),
507
494
  ) -> Any:
508
- api_key = await _resolve_api_key(self)
495
+ api_key = await resolve_api_key(tool=self, provider_name="ElevenLabs", env_var="ELEVENLABS_API_KEY")
509
496
  import base64
510
497
 
511
498
  import httpx
@@ -1,4 +1,3 @@
1
- import os
2
1
  from typing import Annotated, Any
3
2
  from urllib.parse import quote, urlparse
4
3
 
@@ -6,28 +5,11 @@ from pydantic import Field, SecretStr
6
5
 
7
6
  from ..core.tool import Tool
8
7
  from ..platform.integrations import Integration
8
+ from ._creds import resolve_api_key
9
9
 
10
10
  _QUEUE_BASE = "https://queue.fal.run"
11
11
 
12
12
 
13
- async def _resolve_fal_key(tool: Any) -> str:
14
- """Resolve fal API key from integration, explicit field, or env var."""
15
- if isinstance(tool.integration, Integration):
16
- credentials = await tool.integration.resolve()
17
- key = credentials.get("api_key")
18
- if key:
19
- return str(key)
20
- if tool.api_key is not None:
21
- return tool.api_key.get_secret_value()
22
- env_key = os.getenv("FAL_KEY")
23
- if env_key:
24
- return env_key
25
- raise ValueError(
26
- "fal API key not found. Set FAL_KEY environment variable, "
27
- "pass api_key in config, or configure an integration."
28
- )
29
-
30
-
31
13
  def _fal_auth_headers(api_key: str) -> dict[str, str]:
32
14
  return {"Authorization": f"Key {api_key}", "Content-Type": "application/json"}
33
15
 
@@ -91,7 +73,7 @@ class FalQueueSubmit(Tool):
91
73
  description="If set, fal POSTs the result to this URL when the job completes.",
92
74
  ),
93
75
  ) -> dict[str, Any]:
94
- api_key = await _resolve_fal_key(self)
76
+ api_key = await resolve_api_key(tool=self, provider_name="fal", env_var="FAL_KEY")
95
77
  import httpx
96
78
 
97
79
  path = _encode_model_path(model_id)
@@ -149,7 +131,7 @@ class FalQueueStatus(Tool):
149
131
  request_id: str = Field(..., description="request_id returned by fal_queue_submit."),
150
132
  include_logs: bool = Field(False, description="If True, append ?logs=1 for runner log lines."),
151
133
  ) -> dict[str, Any]:
152
- api_key = await _resolve_fal_key(self)
134
+ api_key = await resolve_api_key(tool=self, provider_name="fal", env_var="FAL_KEY")
153
135
  import httpx
154
136
 
155
137
  path = _encode_model_path(model_id)
@@ -197,7 +179,7 @@ class FalQueueResult(Tool):
197
179
  ),
198
180
  request_id: str = Field(..., description="request_id returned by fal_queue_submit."),
199
181
  ) -> dict[str, Any]:
200
- api_key = await _resolve_fal_key(self)
182
+ api_key = await resolve_api_key(tool=self, provider_name="fal", env_var="FAL_KEY")
201
183
  import httpx
202
184
 
203
185
  path = _encode_model_path(model_id)
@@ -242,7 +224,7 @@ class FalQueueCancel(Tool):
242
224
  ),
243
225
  request_id: str = Field(..., description="request_id returned by fal_queue_submit."),
244
226
  ) -> dict[str, Any]:
245
- api_key = await _resolve_fal_key(self)
227
+ api_key = await resolve_api_key(tool=self, provider_name="fal", env_var="FAL_KEY")
246
228
  import httpx
247
229
 
248
230
  path = _encode_model_path(model_id)