prompture 0.0.38.dev2__tar.gz → 0.0.38.dev3__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 (131) hide show
  1. {prompture-0.0.38.dev2/prompture.egg-info → prompture-0.0.38.dev3}/PKG-INFO +1 -1
  2. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/_version.py +2 -2
  3. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_azure_driver.py +1 -1
  4. prompture-0.0.38.dev3/prompture/drivers/async_claude_driver.py +272 -0
  5. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_grok_driver.py +1 -1
  6. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_groq_driver.py +1 -1
  7. prompture-0.0.38.dev3/prompture/drivers/async_openai_driver.py +244 -0
  8. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_openrouter_driver.py +1 -1
  9. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3/prompture.egg-info}/PKG-INFO +1 -1
  10. prompture-0.0.38.dev2/prompture/drivers/async_claude_driver.py +0 -113
  11. prompture-0.0.38.dev2/prompture/drivers/async_openai_driver.py +0 -102
  12. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-driver/SKILL.md +0 -0
  13. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-driver/references/driver-template.md +0 -0
  14. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-example/SKILL.md +0 -0
  15. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-field/SKILL.md +0 -0
  16. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-test/SKILL.md +0 -0
  17. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/run-tests/SKILL.md +0 -0
  18. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/scaffold-extraction/SKILL.md +0 -0
  19. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/update-pricing/SKILL.md +0 -0
  20. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.env.copy +0 -0
  21. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/FUNDING.yml +0 -0
  22. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/scripts/update_docs_version.py +0 -0
  23. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/scripts/update_wrapper_version.py +0 -0
  24. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/workflows/dev.yml +0 -0
  25. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/workflows/documentation.yml +0 -0
  26. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/workflows/publish.yml +0 -0
  27. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/CLAUDE.md +0 -0
  28. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/LICENSE +0 -0
  29. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/MANIFEST.in +0 -0
  30. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/README.md +0 -0
  31. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/ROADMAP.md +0 -0
  32. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/_static/custom.css +0 -0
  33. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/_templates/footer.html +0 -0
  34. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/core.rst +0 -0
  35. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/drivers.rst +0 -0
  36. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/field_definitions.rst +0 -0
  37. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/index.rst +0 -0
  38. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/runner.rst +0 -0
  39. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/tools.rst +0 -0
  40. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/validator.rst +0 -0
  41. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/conf.py +0 -0
  42. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/contributing.rst +0 -0
  43. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/examples.rst +0 -0
  44. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/field_definitions_reference.rst +0 -0
  45. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/index.rst +0 -0
  46. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/installation.rst +0 -0
  47. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/quickstart.rst +0 -0
  48. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/toon_input_guide.rst +0 -0
  49. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/README.md +0 -0
  50. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_json/README.md +0 -0
  51. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_json/llm_to_json/__init__.py +0 -0
  52. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_json/pyproject.toml +0 -0
  53. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_json/test.py +0 -0
  54. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_toon/README.md +0 -0
  55. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_toon/llm_to_toon/__init__.py +0 -0
  56. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_toon/pyproject.toml +0 -0
  57. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_toon/test.py +0 -0
  58. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/__init__.py +0 -0
  59. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/agent.py +0 -0
  60. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/agent_types.py +0 -0
  61. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/aio/__init__.py +0 -0
  62. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_agent.py +0 -0
  63. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_conversation.py +0 -0
  64. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_core.py +0 -0
  65. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_driver.py +0 -0
  66. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_groups.py +0 -0
  67. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/cache.py +0 -0
  68. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/callbacks.py +0 -0
  69. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/cli.py +0 -0
  70. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/conversation.py +0 -0
  71. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/core.py +0 -0
  72. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/cost_mixin.py +0 -0
  73. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/discovery.py +0 -0
  74. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/driver.py +0 -0
  75. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/__init__.py +0 -0
  76. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/airllm_driver.py +0 -0
  77. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_airllm_driver.py +0 -0
  78. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_google_driver.py +0 -0
  79. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_hugging_driver.py +0 -0
  80. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_lmstudio_driver.py +0 -0
  81. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_local_http_driver.py +0 -0
  82. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_ollama_driver.py +0 -0
  83. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_registry.py +0 -0
  84. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/azure_driver.py +0 -0
  85. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/claude_driver.py +0 -0
  86. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/google_driver.py +0 -0
  87. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/grok_driver.py +0 -0
  88. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/groq_driver.py +0 -0
  89. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/hugging_driver.py +0 -0
  90. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/lmstudio_driver.py +0 -0
  91. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/local_http_driver.py +0 -0
  92. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/ollama_driver.py +0 -0
  93. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/openai_driver.py +0 -0
  94. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/openrouter_driver.py +0 -0
  95. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/registry.py +0 -0
  96. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/vision_helpers.py +0 -0
  97. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/field_definitions.py +0 -0
  98. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/group_types.py +0 -0
  99. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/groups.py +0 -0
  100. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/image.py +0 -0
  101. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/logging.py +0 -0
  102. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/model_rates.py +0 -0
  103. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/persistence.py +0 -0
  104. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/persona.py +0 -0
  105. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/runner.py +0 -0
  106. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/__init__.py +0 -0
  107. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/generator.py +0 -0
  108. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/Dockerfile.j2 +0 -0
  109. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/README.md.j2 +0 -0
  110. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/config.py.j2 +0 -0
  111. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/env.example.j2 +0 -0
  112. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/main.py.j2 +0 -0
  113. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/models.py.j2 +0 -0
  114. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/requirements.txt.j2 +0 -0
  115. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/serialization.py +0 -0
  116. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/server.py +0 -0
  117. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/session.py +0 -0
  118. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/settings.py +0 -0
  119. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/tools.py +0 -0
  120. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/tools_schema.py +0 -0
  121. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/validator.py +0 -0
  122. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/SOURCES.txt +0 -0
  123. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/dependency_links.txt +0 -0
  124. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/entry_points.txt +0 -0
  125. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/requires.txt +0 -0
  126. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/top_level.txt +0 -0
  127. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/pyproject.toml +0 -0
  128. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/requirements.txt +0 -0
  129. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/setup.cfg +0 -0
  130. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/test.py +0 -0
  131. {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/test_version_diagnosis.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompture
3
- Version: 0.0.38.dev2
3
+ Version: 0.0.38.dev3
4
4
  Summary: Ask LLMs to return structured JSON and run cross-model tests. API-first.
5
5
  Author-email: Juan Denis <juan@vene.co>
6
6
  License-Expression: MIT
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.38.dev2'
32
- __version_tuple__ = version_tuple = (0, 0, 38, 'dev2')
31
+ __version__ = version = '0.0.38.dev3'
32
+ __version_tuple__ = version_tuple = (0, 0, 38, 'dev3')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -113,7 +113,7 @@ class AsyncAzureDriver(CostMixin, AsyncDriver):
113
113
  "prompt_tokens": prompt_tokens,
114
114
  "completion_tokens": completion_tokens,
115
115
  "total_tokens": total_tokens,
116
- "cost": total_cost,
116
+ "cost": round(total_cost, 6),
117
117
  "raw_response": resp.model_dump(),
118
118
  "model_name": model,
119
119
  "deployment_id": self.deployment_id,
@@ -0,0 +1,272 @@
1
+ """Async Anthropic Claude driver. Requires the ``anthropic`` package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from collections.abc import AsyncIterator
8
+ from typing import Any
9
+
10
+ try:
11
+ import anthropic
12
+ except Exception:
13
+ anthropic = None
14
+
15
+ from ..async_driver import AsyncDriver
16
+ from ..cost_mixin import CostMixin
17
+ from .claude_driver import ClaudeDriver
18
+
19
+
20
+ class AsyncClaudeDriver(CostMixin, AsyncDriver):
21
+ supports_json_mode = True
22
+ supports_json_schema = True
23
+ supports_tool_use = True
24
+ supports_streaming = True
25
+ supports_vision = True
26
+
27
+ MODEL_PRICING = ClaudeDriver.MODEL_PRICING
28
+
29
+ def __init__(self, api_key: str | None = None, model: str = "claude-3-5-haiku-20241022"):
30
+ self.api_key = api_key or os.getenv("CLAUDE_API_KEY")
31
+ self.model = model or os.getenv("CLAUDE_MODEL_NAME", "claude-3-5-haiku-20241022")
32
+
33
+ supports_messages = True
34
+
35
+ def _prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
36
+ from .vision_helpers import _prepare_claude_vision_messages
37
+
38
+ return _prepare_claude_vision_messages(messages)
39
+
40
+ async def generate(self, prompt: str, options: dict[str, Any]) -> dict[str, Any]:
41
+ messages = [{"role": "user", "content": prompt}]
42
+ return await self._do_generate(messages, options)
43
+
44
+ async def generate_messages(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
45
+ return await self._do_generate(self._prepare_messages(messages), options)
46
+
47
+ async def _do_generate(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
48
+ if anthropic is None:
49
+ raise RuntimeError("anthropic package not installed")
50
+
51
+ opts = {**{"temperature": 0.0, "max_tokens": 512}, **options}
52
+ model = options.get("model", self.model)
53
+
54
+ client = anthropic.AsyncAnthropic(api_key=self.api_key)
55
+
56
+ # Anthropic requires system messages as a top-level parameter
57
+ system_content, api_messages = self._extract_system_and_messages(messages)
58
+
59
+ # Build common kwargs
60
+ common_kwargs: dict[str, Any] = {
61
+ "model": model,
62
+ "messages": api_messages,
63
+ "temperature": opts["temperature"],
64
+ "max_tokens": opts["max_tokens"],
65
+ }
66
+ if system_content:
67
+ common_kwargs["system"] = system_content
68
+
69
+ # Native JSON mode: use tool-use for schema enforcement
70
+ if options.get("json_mode"):
71
+ json_schema = options.get("json_schema")
72
+ if json_schema:
73
+ tool_def = {
74
+ "name": "extract_json",
75
+ "description": "Extract structured data matching the schema",
76
+ "input_schema": json_schema,
77
+ }
78
+ resp = await client.messages.create(
79
+ **common_kwargs,
80
+ tools=[tool_def],
81
+ tool_choice={"type": "tool", "name": "extract_json"},
82
+ )
83
+ text = ""
84
+ for block in resp.content:
85
+ if block.type == "tool_use":
86
+ text = json.dumps(block.input)
87
+ break
88
+ else:
89
+ resp = await client.messages.create(**common_kwargs)
90
+ text = resp.content[0].text
91
+ else:
92
+ resp = await client.messages.create(**common_kwargs)
93
+ text = resp.content[0].text
94
+
95
+ prompt_tokens = resp.usage.input_tokens
96
+ completion_tokens = resp.usage.output_tokens
97
+ total_tokens = prompt_tokens + completion_tokens
98
+
99
+ total_cost = self._calculate_cost("claude", model, prompt_tokens, completion_tokens)
100
+
101
+ meta = {
102
+ "prompt_tokens": prompt_tokens,
103
+ "completion_tokens": completion_tokens,
104
+ "total_tokens": total_tokens,
105
+ "cost": round(total_cost, 6),
106
+ "raw_response": dict(resp),
107
+ "model_name": model,
108
+ }
109
+
110
+ return {"text": text, "meta": meta}
111
+
112
+ # ------------------------------------------------------------------
113
+ # Helpers
114
+ # ------------------------------------------------------------------
115
+
116
+ def _extract_system_and_messages(
117
+ self, messages: list[dict[str, Any]]
118
+ ) -> tuple[str | None, list[dict[str, Any]]]:
119
+ """Separate system message from conversation messages for Anthropic API."""
120
+ system_content = None
121
+ api_messages: list[dict[str, Any]] = []
122
+ for msg in messages:
123
+ if msg.get("role") == "system":
124
+ system_content = msg.get("content", "")
125
+ else:
126
+ api_messages.append(msg)
127
+ return system_content, api_messages
128
+
129
+ # ------------------------------------------------------------------
130
+ # Tool use
131
+ # ------------------------------------------------------------------
132
+
133
+ async def generate_messages_with_tools(
134
+ self,
135
+ messages: list[dict[str, Any]],
136
+ tools: list[dict[str, Any]],
137
+ options: dict[str, Any],
138
+ ) -> dict[str, Any]:
139
+ """Generate a response that may include tool calls (Anthropic)."""
140
+ if anthropic is None:
141
+ raise RuntimeError("anthropic package not installed")
142
+
143
+ opts = {**{"temperature": 0.0, "max_tokens": 512}, **options}
144
+ model = options.get("model", self.model)
145
+ client = anthropic.AsyncAnthropic(api_key=self.api_key)
146
+
147
+ system_content, api_messages = self._extract_system_and_messages(messages)
148
+
149
+ # Convert tools from OpenAI format to Anthropic format if needed
150
+ anthropic_tools = []
151
+ for t in tools:
152
+ if "type" in t and t["type"] == "function":
153
+ # OpenAI format -> Anthropic format
154
+ fn = t["function"]
155
+ anthropic_tools.append({
156
+ "name": fn["name"],
157
+ "description": fn.get("description", ""),
158
+ "input_schema": fn.get("parameters", {"type": "object", "properties": {}}),
159
+ })
160
+ elif "input_schema" in t:
161
+ # Already Anthropic format
162
+ anthropic_tools.append(t)
163
+ else:
164
+ anthropic_tools.append(t)
165
+
166
+ kwargs: dict[str, Any] = {
167
+ "model": model,
168
+ "messages": api_messages,
169
+ "temperature": opts["temperature"],
170
+ "max_tokens": opts["max_tokens"],
171
+ "tools": anthropic_tools,
172
+ }
173
+ if system_content:
174
+ kwargs["system"] = system_content
175
+
176
+ resp = await client.messages.create(**kwargs)
177
+
178
+ prompt_tokens = resp.usage.input_tokens
179
+ completion_tokens = resp.usage.output_tokens
180
+ total_tokens = prompt_tokens + completion_tokens
181
+ total_cost = self._calculate_cost("claude", model, prompt_tokens, completion_tokens)
182
+
183
+ meta = {
184
+ "prompt_tokens": prompt_tokens,
185
+ "completion_tokens": completion_tokens,
186
+ "total_tokens": total_tokens,
187
+ "cost": round(total_cost, 6),
188
+ "raw_response": dict(resp),
189
+ "model_name": model,
190
+ }
191
+
192
+ text = ""
193
+ tool_calls_out: list[dict[str, Any]] = []
194
+ for block in resp.content:
195
+ if block.type == "text":
196
+ text += block.text
197
+ elif block.type == "tool_use":
198
+ tool_calls_out.append({
199
+ "id": block.id,
200
+ "name": block.name,
201
+ "arguments": block.input,
202
+ })
203
+
204
+ return {
205
+ "text": text,
206
+ "meta": meta,
207
+ "tool_calls": tool_calls_out,
208
+ "stop_reason": resp.stop_reason,
209
+ }
210
+
211
+ # ------------------------------------------------------------------
212
+ # Streaming
213
+ # ------------------------------------------------------------------
214
+
215
+ async def generate_messages_stream(
216
+ self,
217
+ messages: list[dict[str, Any]],
218
+ options: dict[str, Any],
219
+ ) -> AsyncIterator[dict[str, Any]]:
220
+ """Yield response chunks via Anthropic streaming API."""
221
+ if anthropic is None:
222
+ raise RuntimeError("anthropic package not installed")
223
+
224
+ opts = {**{"temperature": 0.0, "max_tokens": 512}, **options}
225
+ model = options.get("model", self.model)
226
+ client = anthropic.AsyncAnthropic(api_key=self.api_key)
227
+
228
+ system_content, api_messages = self._extract_system_and_messages(messages)
229
+
230
+ kwargs: dict[str, Any] = {
231
+ "model": model,
232
+ "messages": api_messages,
233
+ "temperature": opts["temperature"],
234
+ "max_tokens": opts["max_tokens"],
235
+ }
236
+ if system_content:
237
+ kwargs["system"] = system_content
238
+
239
+ full_text = ""
240
+ prompt_tokens = 0
241
+ completion_tokens = 0
242
+
243
+ async with client.messages.stream(**kwargs) as stream:
244
+ async for event in stream:
245
+ if hasattr(event, "type"):
246
+ if event.type == "content_block_delta" and hasattr(event, "delta"):
247
+ delta_text = getattr(event.delta, "text", "")
248
+ if delta_text:
249
+ full_text += delta_text
250
+ yield {"type": "delta", "text": delta_text}
251
+ elif event.type == "message_delta" and hasattr(event, "usage"):
252
+ completion_tokens = getattr(event.usage, "output_tokens", 0)
253
+ elif event.type == "message_start" and hasattr(event, "message"):
254
+ usage = getattr(event.message, "usage", None)
255
+ if usage:
256
+ prompt_tokens = getattr(usage, "input_tokens", 0)
257
+
258
+ total_tokens = prompt_tokens + completion_tokens
259
+ total_cost = self._calculate_cost("claude", model, prompt_tokens, completion_tokens)
260
+
261
+ yield {
262
+ "type": "done",
263
+ "text": full_text,
264
+ "meta": {
265
+ "prompt_tokens": prompt_tokens,
266
+ "completion_tokens": completion_tokens,
267
+ "total_tokens": total_tokens,
268
+ "cost": round(total_cost, 6),
269
+ "raw_response": {},
270
+ "model_name": model,
271
+ },
272
+ }
@@ -88,7 +88,7 @@ class AsyncGrokDriver(CostMixin, AsyncDriver):
88
88
  "prompt_tokens": prompt_tokens,
89
89
  "completion_tokens": completion_tokens,
90
90
  "total_tokens": total_tokens,
91
- "cost": total_cost,
91
+ "cost": round(total_cost, 6),
92
92
  "raw_response": resp,
93
93
  "model_name": model,
94
94
  }
@@ -81,7 +81,7 @@ class AsyncGroqDriver(CostMixin, AsyncDriver):
81
81
  "prompt_tokens": prompt_tokens,
82
82
  "completion_tokens": completion_tokens,
83
83
  "total_tokens": total_tokens,
84
- "cost": total_cost,
84
+ "cost": round(total_cost, 6),
85
85
  "raw_response": resp.model_dump(),
86
86
  "model_name": model,
87
87
  }
@@ -0,0 +1,244 @@
1
+ """Async OpenAI driver. Requires the ``openai`` package (>=1.0.0)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from collections.abc import AsyncIterator
8
+ from typing import Any
9
+
10
+ try:
11
+ from openai import AsyncOpenAI
12
+ except Exception:
13
+ AsyncOpenAI = None
14
+
15
+ from ..async_driver import AsyncDriver
16
+ from ..cost_mixin import CostMixin
17
+ from .openai_driver import OpenAIDriver
18
+
19
+
20
+ class AsyncOpenAIDriver(CostMixin, AsyncDriver):
21
+ supports_json_mode = True
22
+ supports_json_schema = True
23
+ supports_tool_use = True
24
+ supports_streaming = True
25
+ supports_vision = True
26
+
27
+ MODEL_PRICING = OpenAIDriver.MODEL_PRICING
28
+
29
+ def __init__(self, api_key: str | None = None, model: str = "gpt-4o-mini"):
30
+ self.api_key = api_key or os.getenv("OPENAI_API_KEY")
31
+ self.model = model
32
+ if AsyncOpenAI:
33
+ self.client = AsyncOpenAI(api_key=self.api_key)
34
+ else:
35
+ self.client = None
36
+
37
+ supports_messages = True
38
+
39
+ def _prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
40
+ from .vision_helpers import _prepare_openai_vision_messages
41
+
42
+ return _prepare_openai_vision_messages(messages)
43
+
44
+ async def generate(self, prompt: str, options: dict[str, Any]) -> dict[str, Any]:
45
+ messages = [{"role": "user", "content": prompt}]
46
+ return await self._do_generate(messages, options)
47
+
48
+ async def generate_messages(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
49
+ return await self._do_generate(self._prepare_messages(messages), options)
50
+
51
+ async def _do_generate(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
52
+ if self.client is None:
53
+ raise RuntimeError("openai package (>=1.0.0) is not installed")
54
+
55
+ model = options.get("model", self.model)
56
+
57
+ model_info = self.MODEL_PRICING.get(model, {})
58
+ tokens_param = model_info.get("tokens_param", "max_tokens")
59
+ supports_temperature = model_info.get("supports_temperature", True)
60
+
61
+ opts = {"temperature": 1.0, "max_tokens": 512, **options}
62
+
63
+ kwargs = {
64
+ "model": model,
65
+ "messages": messages,
66
+ }
67
+ kwargs[tokens_param] = opts.get("max_tokens", 512)
68
+
69
+ if supports_temperature and "temperature" in opts:
70
+ kwargs["temperature"] = opts["temperature"]
71
+
72
+ # Native JSON mode support
73
+ if options.get("json_mode"):
74
+ json_schema = options.get("json_schema")
75
+ if json_schema:
76
+ kwargs["response_format"] = {
77
+ "type": "json_schema",
78
+ "json_schema": {
79
+ "name": "extraction",
80
+ "strict": True,
81
+ "schema": json_schema,
82
+ },
83
+ }
84
+ else:
85
+ kwargs["response_format"] = {"type": "json_object"}
86
+
87
+ resp = await self.client.chat.completions.create(**kwargs)
88
+
89
+ usage = getattr(resp, "usage", None)
90
+ prompt_tokens = getattr(usage, "prompt_tokens", 0)
91
+ completion_tokens = getattr(usage, "completion_tokens", 0)
92
+ total_tokens = getattr(usage, "total_tokens", 0)
93
+
94
+ total_cost = self._calculate_cost("openai", model, prompt_tokens, completion_tokens)
95
+
96
+ meta = {
97
+ "prompt_tokens": prompt_tokens,
98
+ "completion_tokens": completion_tokens,
99
+ "total_tokens": total_tokens,
100
+ "cost": round(total_cost, 6),
101
+ "raw_response": resp.model_dump(),
102
+ "model_name": model,
103
+ }
104
+
105
+ text = resp.choices[0].message.content
106
+ return {"text": text, "meta": meta}
107
+
108
+ # ------------------------------------------------------------------
109
+ # Tool use
110
+ # ------------------------------------------------------------------
111
+
112
+ async def generate_messages_with_tools(
113
+ self,
114
+ messages: list[dict[str, Any]],
115
+ tools: list[dict[str, Any]],
116
+ options: dict[str, Any],
117
+ ) -> dict[str, Any]:
118
+ """Generate a response that may include tool calls."""
119
+ if self.client is None:
120
+ raise RuntimeError("openai package (>=1.0.0) is not installed")
121
+
122
+ model = options.get("model", self.model)
123
+ model_info = self.MODEL_PRICING.get(model, {})
124
+ tokens_param = model_info.get("tokens_param", "max_tokens")
125
+ supports_temperature = model_info.get("supports_temperature", True)
126
+
127
+ opts = {"temperature": 1.0, "max_tokens": 512, **options}
128
+
129
+ kwargs: dict[str, Any] = {
130
+ "model": model,
131
+ "messages": messages,
132
+ "tools": tools,
133
+ }
134
+ kwargs[tokens_param] = opts.get("max_tokens", 512)
135
+
136
+ if supports_temperature and "temperature" in opts:
137
+ kwargs["temperature"] = opts["temperature"]
138
+
139
+ resp = await self.client.chat.completions.create(**kwargs)
140
+
141
+ usage = getattr(resp, "usage", None)
142
+ prompt_tokens = getattr(usage, "prompt_tokens", 0)
143
+ completion_tokens = getattr(usage, "completion_tokens", 0)
144
+ total_tokens = getattr(usage, "total_tokens", 0)
145
+ total_cost = self._calculate_cost("openai", model, prompt_tokens, completion_tokens)
146
+
147
+ meta = {
148
+ "prompt_tokens": prompt_tokens,
149
+ "completion_tokens": completion_tokens,
150
+ "total_tokens": total_tokens,
151
+ "cost": round(total_cost, 6),
152
+ "raw_response": resp.model_dump(),
153
+ "model_name": model,
154
+ }
155
+
156
+ choice = resp.choices[0]
157
+ text = choice.message.content or ""
158
+ stop_reason = choice.finish_reason
159
+
160
+ tool_calls_out: list[dict[str, Any]] = []
161
+ if choice.message.tool_calls:
162
+ for tc in choice.message.tool_calls:
163
+ try:
164
+ args = json.loads(tc.function.arguments)
165
+ except (json.JSONDecodeError, TypeError):
166
+ args = {}
167
+ tool_calls_out.append({
168
+ "id": tc.id,
169
+ "name": tc.function.name,
170
+ "arguments": args,
171
+ })
172
+
173
+ return {
174
+ "text": text,
175
+ "meta": meta,
176
+ "tool_calls": tool_calls_out,
177
+ "stop_reason": stop_reason,
178
+ }
179
+
180
+ # ------------------------------------------------------------------
181
+ # Streaming
182
+ # ------------------------------------------------------------------
183
+
184
+ async def generate_messages_stream(
185
+ self,
186
+ messages: list[dict[str, Any]],
187
+ options: dict[str, Any],
188
+ ) -> AsyncIterator[dict[str, Any]]:
189
+ """Yield response chunks via OpenAI streaming API."""
190
+ if self.client is None:
191
+ raise RuntimeError("openai package (>=1.0.0) is not installed")
192
+
193
+ model = options.get("model", self.model)
194
+ model_info = self.MODEL_PRICING.get(model, {})
195
+ tokens_param = model_info.get("tokens_param", "max_tokens")
196
+ supports_temperature = model_info.get("supports_temperature", True)
197
+
198
+ opts = {"temperature": 1.0, "max_tokens": 512, **options}
199
+
200
+ kwargs: dict[str, Any] = {
201
+ "model": model,
202
+ "messages": messages,
203
+ "stream": True,
204
+ "stream_options": {"include_usage": True},
205
+ }
206
+ kwargs[tokens_param] = opts.get("max_tokens", 512)
207
+
208
+ if supports_temperature and "temperature" in opts:
209
+ kwargs["temperature"] = opts["temperature"]
210
+
211
+ stream = await self.client.chat.completions.create(**kwargs)
212
+
213
+ full_text = ""
214
+ prompt_tokens = 0
215
+ completion_tokens = 0
216
+
217
+ async for chunk in stream:
218
+ # Usage comes in the final chunk
219
+ if getattr(chunk, "usage", None):
220
+ prompt_tokens = chunk.usage.prompt_tokens or 0
221
+ completion_tokens = chunk.usage.completion_tokens or 0
222
+
223
+ if chunk.choices:
224
+ delta = chunk.choices[0].delta
225
+ content = getattr(delta, "content", None) or ""
226
+ if content:
227
+ full_text += content
228
+ yield {"type": "delta", "text": content}
229
+
230
+ total_tokens = prompt_tokens + completion_tokens
231
+ total_cost = self._calculate_cost("openai", model, prompt_tokens, completion_tokens)
232
+
233
+ yield {
234
+ "type": "done",
235
+ "text": full_text,
236
+ "meta": {
237
+ "prompt_tokens": prompt_tokens,
238
+ "completion_tokens": completion_tokens,
239
+ "total_tokens": total_tokens,
240
+ "cost": round(total_cost, 6),
241
+ "raw_response": {},
242
+ "model_name": model,
243
+ },
244
+ }
@@ -93,7 +93,7 @@ class AsyncOpenRouterDriver(CostMixin, AsyncDriver):
93
93
  "prompt_tokens": prompt_tokens,
94
94
  "completion_tokens": completion_tokens,
95
95
  "total_tokens": total_tokens,
96
- "cost": total_cost,
96
+ "cost": round(total_cost, 6),
97
97
  "raw_response": resp,
98
98
  "model_name": model,
99
99
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompture
3
- Version: 0.0.38.dev2
3
+ Version: 0.0.38.dev3
4
4
  Summary: Ask LLMs to return structured JSON and run cross-model tests. API-first.
5
5
  Author-email: Juan Denis <juan@vene.co>
6
6
  License-Expression: MIT