prompture 0.0.38__tar.gz → 0.0.38.dev1__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 (133) hide show
  1. {prompture-0.0.38/prompture.egg-info → prompture-0.0.38.dev1}/PKG-INFO +1 -1
  2. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/conf.py +1 -1
  3. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/_version.py +2 -2
  4. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_azure_driver.py +1 -1
  5. prompture-0.0.38.dev1/prompture/drivers/async_claude_driver.py +113 -0
  6. prompture-0.0.38.dev1/prompture/drivers/async_google_driver.py +152 -0
  7. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_grok_driver.py +1 -1
  8. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_groq_driver.py +1 -1
  9. prompture-0.0.38.dev1/prompture/drivers/async_openai_driver.py +102 -0
  10. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_openrouter_driver.py +1 -1
  11. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/google_driver.py +43 -207
  12. {prompture-0.0.38 → prompture-0.0.38.dev1/prompture.egg-info}/PKG-INFO +1 -1
  13. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture.egg-info/SOURCES.txt +0 -2
  14. prompture-0.0.38/VERSION +0 -1
  15. prompture-0.0.38/docs/source/_templates/footer.html +0 -16
  16. prompture-0.0.38/prompture/drivers/async_claude_driver.py +0 -272
  17. prompture-0.0.38/prompture/drivers/async_google_driver.py +0 -316
  18. prompture-0.0.38/prompture/drivers/async_openai_driver.py +0 -244
  19. {prompture-0.0.38 → prompture-0.0.38.dev1}/.claude/skills/add-driver/SKILL.md +0 -0
  20. {prompture-0.0.38 → prompture-0.0.38.dev1}/.claude/skills/add-driver/references/driver-template.md +0 -0
  21. {prompture-0.0.38 → prompture-0.0.38.dev1}/.claude/skills/add-example/SKILL.md +0 -0
  22. {prompture-0.0.38 → prompture-0.0.38.dev1}/.claude/skills/add-field/SKILL.md +0 -0
  23. {prompture-0.0.38 → prompture-0.0.38.dev1}/.claude/skills/add-test/SKILL.md +0 -0
  24. {prompture-0.0.38 → prompture-0.0.38.dev1}/.claude/skills/run-tests/SKILL.md +0 -0
  25. {prompture-0.0.38 → prompture-0.0.38.dev1}/.claude/skills/scaffold-extraction/SKILL.md +0 -0
  26. {prompture-0.0.38 → prompture-0.0.38.dev1}/.claude/skills/update-pricing/SKILL.md +0 -0
  27. {prompture-0.0.38 → prompture-0.0.38.dev1}/.env.copy +0 -0
  28. {prompture-0.0.38 → prompture-0.0.38.dev1}/.github/FUNDING.yml +0 -0
  29. {prompture-0.0.38 → prompture-0.0.38.dev1}/.github/scripts/update_docs_version.py +0 -0
  30. {prompture-0.0.38 → prompture-0.0.38.dev1}/.github/scripts/update_wrapper_version.py +0 -0
  31. {prompture-0.0.38 → prompture-0.0.38.dev1}/.github/workflows/dev.yml +0 -0
  32. {prompture-0.0.38 → prompture-0.0.38.dev1}/.github/workflows/documentation.yml +0 -0
  33. {prompture-0.0.38 → prompture-0.0.38.dev1}/.github/workflows/publish.yml +0 -0
  34. {prompture-0.0.38 → prompture-0.0.38.dev1}/CLAUDE.md +0 -0
  35. {prompture-0.0.38 → prompture-0.0.38.dev1}/LICENSE +0 -0
  36. {prompture-0.0.38 → prompture-0.0.38.dev1}/MANIFEST.in +0 -0
  37. {prompture-0.0.38 → prompture-0.0.38.dev1}/README.md +0 -0
  38. {prompture-0.0.38 → prompture-0.0.38.dev1}/ROADMAP.md +0 -0
  39. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/_static/custom.css +0 -0
  40. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/api/core.rst +0 -0
  41. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/api/drivers.rst +0 -0
  42. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/api/field_definitions.rst +0 -0
  43. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/api/index.rst +0 -0
  44. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/api/runner.rst +0 -0
  45. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/api/tools.rst +0 -0
  46. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/api/validator.rst +0 -0
  47. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/contributing.rst +0 -0
  48. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/examples.rst +0 -0
  49. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/field_definitions_reference.rst +0 -0
  50. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/index.rst +0 -0
  51. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/installation.rst +0 -0
  52. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/quickstart.rst +0 -0
  53. {prompture-0.0.38 → prompture-0.0.38.dev1}/docs/source/toon_input_guide.rst +0 -0
  54. {prompture-0.0.38 → prompture-0.0.38.dev1}/packages/README.md +0 -0
  55. {prompture-0.0.38 → prompture-0.0.38.dev1}/packages/llm_to_json/README.md +0 -0
  56. {prompture-0.0.38 → prompture-0.0.38.dev1}/packages/llm_to_json/llm_to_json/__init__.py +0 -0
  57. {prompture-0.0.38 → prompture-0.0.38.dev1}/packages/llm_to_json/pyproject.toml +0 -0
  58. {prompture-0.0.38 → prompture-0.0.38.dev1}/packages/llm_to_json/test.py +0 -0
  59. {prompture-0.0.38 → prompture-0.0.38.dev1}/packages/llm_to_toon/README.md +0 -0
  60. {prompture-0.0.38 → prompture-0.0.38.dev1}/packages/llm_to_toon/llm_to_toon/__init__.py +0 -0
  61. {prompture-0.0.38 → prompture-0.0.38.dev1}/packages/llm_to_toon/pyproject.toml +0 -0
  62. {prompture-0.0.38 → prompture-0.0.38.dev1}/packages/llm_to_toon/test.py +0 -0
  63. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/__init__.py +0 -0
  64. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/agent.py +0 -0
  65. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/agent_types.py +0 -0
  66. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/aio/__init__.py +0 -0
  67. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/async_agent.py +0 -0
  68. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/async_conversation.py +0 -0
  69. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/async_core.py +0 -0
  70. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/async_driver.py +0 -0
  71. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/async_groups.py +0 -0
  72. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/cache.py +0 -0
  73. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/callbacks.py +0 -0
  74. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/cli.py +0 -0
  75. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/conversation.py +0 -0
  76. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/core.py +0 -0
  77. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/cost_mixin.py +0 -0
  78. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/discovery.py +0 -0
  79. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/driver.py +0 -0
  80. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/__init__.py +0 -0
  81. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/airllm_driver.py +0 -0
  82. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_airllm_driver.py +0 -0
  83. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_hugging_driver.py +0 -0
  84. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_lmstudio_driver.py +0 -0
  85. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_local_http_driver.py +0 -0
  86. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_ollama_driver.py +0 -0
  87. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/async_registry.py +0 -0
  88. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/azure_driver.py +0 -0
  89. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/claude_driver.py +0 -0
  90. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/grok_driver.py +0 -0
  91. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/groq_driver.py +0 -0
  92. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/hugging_driver.py +0 -0
  93. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/lmstudio_driver.py +0 -0
  94. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/local_http_driver.py +0 -0
  95. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/ollama_driver.py +0 -0
  96. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/openai_driver.py +0 -0
  97. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/openrouter_driver.py +0 -0
  98. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/registry.py +0 -0
  99. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/drivers/vision_helpers.py +0 -0
  100. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/field_definitions.py +0 -0
  101. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/group_types.py +0 -0
  102. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/groups.py +0 -0
  103. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/image.py +0 -0
  104. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/logging.py +0 -0
  105. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/model_rates.py +0 -0
  106. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/persistence.py +0 -0
  107. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/persona.py +0 -0
  108. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/runner.py +0 -0
  109. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/scaffold/__init__.py +0 -0
  110. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/scaffold/generator.py +0 -0
  111. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/scaffold/templates/Dockerfile.j2 +0 -0
  112. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/scaffold/templates/README.md.j2 +0 -0
  113. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/scaffold/templates/config.py.j2 +0 -0
  114. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/scaffold/templates/env.example.j2 +0 -0
  115. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/scaffold/templates/main.py.j2 +0 -0
  116. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/scaffold/templates/models.py.j2 +0 -0
  117. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/scaffold/templates/requirements.txt.j2 +0 -0
  118. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/serialization.py +0 -0
  119. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/server.py +0 -0
  120. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/session.py +0 -0
  121. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/settings.py +0 -0
  122. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/tools.py +0 -0
  123. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/tools_schema.py +0 -0
  124. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture/validator.py +0 -0
  125. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture.egg-info/dependency_links.txt +0 -0
  126. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture.egg-info/entry_points.txt +0 -0
  127. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture.egg-info/requires.txt +0 -0
  128. {prompture-0.0.38 → prompture-0.0.38.dev1}/prompture.egg-info/top_level.txt +0 -0
  129. {prompture-0.0.38 → prompture-0.0.38.dev1}/pyproject.toml +0 -0
  130. {prompture-0.0.38 → prompture-0.0.38.dev1}/requirements.txt +0 -0
  131. {prompture-0.0.38 → prompture-0.0.38.dev1}/setup.cfg +0 -0
  132. {prompture-0.0.38 → prompture-0.0.38.dev1}/test.py +0 -0
  133. {prompture-0.0.38 → prompture-0.0.38.dev1}/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
3
+ Version: 0.0.38.dev1
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
@@ -14,7 +14,7 @@ sys.path.insert(0, os.path.abspath("../../"))
14
14
  # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
15
15
 
16
16
  project = "Prompture"
17
- copyright = '2026, Juan Denis'
17
+ copyright = '2026, <a href="https://juandenis.com">Juan Denis</a>'
18
18
  author = "Juan Denis"
19
19
 
20
20
  # Read version dynamically: VERSION file > setuptools_scm > fallback
@@ -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'
32
- __version_tuple__ = version_tuple = (0, 0, 38)
31
+ __version__ = version = '0.0.38.dev1'
32
+ __version_tuple__ = version_tuple = (0, 0, 38, 'dev1')
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": round(total_cost, 6),
116
+ "cost": total_cost,
117
117
  "raw_response": resp.model_dump(),
118
118
  "model_name": model,
119
119
  "deployment_id": self.deployment_id,
@@ -0,0 +1,113 @@
1
+ """Async Anthropic Claude driver. Requires the ``anthropic`` package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from typing import Any
8
+
9
+ try:
10
+ import anthropic
11
+ except Exception:
12
+ anthropic = None
13
+
14
+ from ..async_driver import AsyncDriver
15
+ from ..cost_mixin import CostMixin
16
+ from .claude_driver import ClaudeDriver
17
+
18
+
19
+ class AsyncClaudeDriver(CostMixin, AsyncDriver):
20
+ supports_json_mode = True
21
+ supports_json_schema = True
22
+ supports_vision = True
23
+
24
+ MODEL_PRICING = ClaudeDriver.MODEL_PRICING
25
+
26
+ def __init__(self, api_key: str | None = None, model: str = "claude-3-5-haiku-20241022"):
27
+ self.api_key = api_key or os.getenv("CLAUDE_API_KEY")
28
+ self.model = model or os.getenv("CLAUDE_MODEL_NAME", "claude-3-5-haiku-20241022")
29
+
30
+ supports_messages = True
31
+
32
+ def _prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
33
+ from .vision_helpers import _prepare_claude_vision_messages
34
+
35
+ return _prepare_claude_vision_messages(messages)
36
+
37
+ async def generate(self, prompt: str, options: dict[str, Any]) -> dict[str, Any]:
38
+ messages = [{"role": "user", "content": prompt}]
39
+ return await self._do_generate(messages, options)
40
+
41
+ async def generate_messages(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
42
+ return await self._do_generate(self._prepare_messages(messages), options)
43
+
44
+ async def _do_generate(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
45
+ if anthropic is None:
46
+ raise RuntimeError("anthropic package not installed")
47
+
48
+ opts = {**{"temperature": 0.0, "max_tokens": 512}, **options}
49
+ model = options.get("model", self.model)
50
+
51
+ client = anthropic.AsyncAnthropic(api_key=self.api_key)
52
+
53
+ # Anthropic requires system messages as a top-level parameter
54
+ system_content = None
55
+ api_messages = []
56
+ for msg in messages:
57
+ if msg.get("role") == "system":
58
+ system_content = msg.get("content", "")
59
+ else:
60
+ api_messages.append(msg)
61
+
62
+ # Build common kwargs
63
+ common_kwargs: dict[str, Any] = {
64
+ "model": model,
65
+ "messages": api_messages,
66
+ "temperature": opts["temperature"],
67
+ "max_tokens": opts["max_tokens"],
68
+ }
69
+ if system_content:
70
+ common_kwargs["system"] = system_content
71
+
72
+ # Native JSON mode: use tool-use for schema enforcement
73
+ if options.get("json_mode"):
74
+ json_schema = options.get("json_schema")
75
+ if json_schema:
76
+ tool_def = {
77
+ "name": "extract_json",
78
+ "description": "Extract structured data matching the schema",
79
+ "input_schema": json_schema,
80
+ }
81
+ resp = await client.messages.create(
82
+ **common_kwargs,
83
+ tools=[tool_def],
84
+ tool_choice={"type": "tool", "name": "extract_json"},
85
+ )
86
+ text = ""
87
+ for block in resp.content:
88
+ if block.type == "tool_use":
89
+ text = json.dumps(block.input)
90
+ break
91
+ else:
92
+ resp = await client.messages.create(**common_kwargs)
93
+ text = resp.content[0].text
94
+ else:
95
+ resp = await client.messages.create(**common_kwargs)
96
+ text = resp.content[0].text
97
+
98
+ prompt_tokens = resp.usage.input_tokens
99
+ completion_tokens = resp.usage.output_tokens
100
+ total_tokens = prompt_tokens + completion_tokens
101
+
102
+ total_cost = self._calculate_cost("claude", model, prompt_tokens, completion_tokens)
103
+
104
+ meta = {
105
+ "prompt_tokens": prompt_tokens,
106
+ "completion_tokens": completion_tokens,
107
+ "total_tokens": total_tokens,
108
+ "cost": total_cost,
109
+ "raw_response": dict(resp),
110
+ "model_name": model,
111
+ }
112
+
113
+ return {"text": text, "meta": meta}
@@ -0,0 +1,152 @@
1
+ """Async Google Generative AI (Gemini) driver."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from typing import Any
8
+
9
+ import google.generativeai as genai
10
+
11
+ from ..async_driver import AsyncDriver
12
+ from ..cost_mixin import CostMixin
13
+ from .google_driver import GoogleDriver
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class AsyncGoogleDriver(CostMixin, AsyncDriver):
19
+ """Async driver for Google's Generative AI API (Gemini)."""
20
+
21
+ supports_json_mode = True
22
+ supports_json_schema = True
23
+ supports_vision = True
24
+
25
+ MODEL_PRICING = GoogleDriver.MODEL_PRICING
26
+ _PRICING_UNIT = 1_000_000
27
+
28
+ def __init__(self, api_key: str | None = None, model: str = "gemini-1.5-pro"):
29
+ self.api_key = api_key or os.getenv("GOOGLE_API_KEY")
30
+ if not self.api_key:
31
+ raise ValueError("Google API key not found. Set GOOGLE_API_KEY env var or pass api_key to constructor")
32
+ self.model = model
33
+ genai.configure(api_key=self.api_key)
34
+ self.options: dict[str, Any] = {}
35
+
36
+ def _calculate_cost_chars(self, prompt_chars: int, completion_chars: int) -> float:
37
+ """Calculate cost from character counts (same logic as sync GoogleDriver)."""
38
+ from ..model_rates import get_model_rates
39
+
40
+ live_rates = get_model_rates("google", self.model)
41
+ if live_rates:
42
+ est_prompt_tokens = prompt_chars / 4
43
+ est_completion_tokens = completion_chars / 4
44
+ prompt_cost = (est_prompt_tokens / 1_000_000) * live_rates["input"]
45
+ completion_cost = (est_completion_tokens / 1_000_000) * live_rates["output"]
46
+ else:
47
+ model_pricing = self.MODEL_PRICING.get(self.model, {"prompt": 0, "completion": 0})
48
+ prompt_cost = (prompt_chars / 1_000_000) * model_pricing["prompt"]
49
+ completion_cost = (completion_chars / 1_000_000) * model_pricing["completion"]
50
+ return round(prompt_cost + completion_cost, 6)
51
+
52
+ supports_messages = True
53
+
54
+ def _prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
55
+ from .vision_helpers import _prepare_google_vision_messages
56
+
57
+ return _prepare_google_vision_messages(messages)
58
+
59
+ async def generate(self, prompt: str, options: dict[str, Any] | None = None) -> dict[str, Any]:
60
+ messages = [{"role": "user", "content": prompt}]
61
+ return await self._do_generate(messages, options)
62
+
63
+ async def generate_messages(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
64
+ return await self._do_generate(self._prepare_messages(messages), options)
65
+
66
+ async def _do_generate(
67
+ self, messages: list[dict[str, str]], options: dict[str, Any] | None = None
68
+ ) -> dict[str, Any]:
69
+ merged_options = self.options.copy()
70
+ if options:
71
+ merged_options.update(options)
72
+
73
+ generation_config = merged_options.get("generation_config", {})
74
+ safety_settings = merged_options.get("safety_settings", {})
75
+
76
+ if "temperature" in merged_options and "temperature" not in generation_config:
77
+ generation_config["temperature"] = merged_options["temperature"]
78
+ if "max_tokens" in merged_options and "max_output_tokens" not in generation_config:
79
+ generation_config["max_output_tokens"] = merged_options["max_tokens"]
80
+ if "top_p" in merged_options and "top_p" not in generation_config:
81
+ generation_config["top_p"] = merged_options["top_p"]
82
+ if "top_k" in merged_options and "top_k" not in generation_config:
83
+ generation_config["top_k"] = merged_options["top_k"]
84
+
85
+ # Native JSON mode support
86
+ if merged_options.get("json_mode"):
87
+ generation_config["response_mime_type"] = "application/json"
88
+ json_schema = merged_options.get("json_schema")
89
+ if json_schema:
90
+ generation_config["response_schema"] = json_schema
91
+
92
+ # Convert messages to Gemini format
93
+ system_instruction = None
94
+ contents: list[dict[str, Any]] = []
95
+ for msg in messages:
96
+ role = msg.get("role", "user")
97
+ content = msg.get("content", "")
98
+ if role == "system":
99
+ system_instruction = content if isinstance(content, str) else str(content)
100
+ else:
101
+ gemini_role = "model" if role == "assistant" else "user"
102
+ if msg.get("_vision_parts"):
103
+ # Already converted to Gemini parts by _prepare_messages
104
+ contents.append({"role": gemini_role, "parts": content})
105
+ else:
106
+ contents.append({"role": gemini_role, "parts": [content]})
107
+
108
+ try:
109
+ model_kwargs: dict[str, Any] = {}
110
+ if system_instruction:
111
+ model_kwargs["system_instruction"] = system_instruction
112
+ model = genai.GenerativeModel(self.model, **model_kwargs)
113
+
114
+ gen_input: Any = contents if len(contents) != 1 else contents[0]["parts"][0]
115
+ response = await model.generate_content_async(
116
+ gen_input,
117
+ generation_config=generation_config if generation_config else None,
118
+ safety_settings=safety_settings if safety_settings else None,
119
+ )
120
+
121
+ if not response.text:
122
+ raise ValueError("Empty response from model")
123
+
124
+ total_prompt_chars = 0
125
+ for msg in messages:
126
+ c = msg.get("content", "")
127
+ if isinstance(c, str):
128
+ total_prompt_chars += len(c)
129
+ elif isinstance(c, list):
130
+ for part in c:
131
+ if isinstance(part, str):
132
+ total_prompt_chars += len(part)
133
+ elif isinstance(part, dict) and "text" in part:
134
+ total_prompt_chars += len(part["text"])
135
+ completion_chars = len(response.text)
136
+
137
+ total_cost = self._calculate_cost_chars(total_prompt_chars, completion_chars)
138
+
139
+ meta = {
140
+ "prompt_chars": total_prompt_chars,
141
+ "completion_chars": completion_chars,
142
+ "total_chars": total_prompt_chars + completion_chars,
143
+ "cost": total_cost,
144
+ "raw_response": response.prompt_feedback if hasattr(response, "prompt_feedback") else None,
145
+ "model_name": self.model,
146
+ }
147
+
148
+ return {"text": response.text, "meta": meta}
149
+
150
+ except Exception as e:
151
+ logger.error(f"Google API request failed: {e}")
152
+ raise RuntimeError(f"Google API request failed: {e}") from e
@@ -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": round(total_cost, 6),
91
+ "cost": total_cost,
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": round(total_cost, 6),
84
+ "cost": total_cost,
85
85
  "raw_response": resp.model_dump(),
86
86
  "model_name": model,
87
87
  }
@@ -0,0 +1,102 @@
1
+ """Async OpenAI driver. Requires the ``openai`` package (>=1.0.0)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any
7
+
8
+ try:
9
+ from openai import AsyncOpenAI
10
+ except Exception:
11
+ AsyncOpenAI = None
12
+
13
+ from ..async_driver import AsyncDriver
14
+ from ..cost_mixin import CostMixin
15
+ from .openai_driver import OpenAIDriver
16
+
17
+
18
+ class AsyncOpenAIDriver(CostMixin, AsyncDriver):
19
+ supports_json_mode = True
20
+ supports_json_schema = True
21
+ supports_vision = True
22
+
23
+ MODEL_PRICING = OpenAIDriver.MODEL_PRICING
24
+
25
+ def __init__(self, api_key: str | None = None, model: str = "gpt-4o-mini"):
26
+ self.api_key = api_key or os.getenv("OPENAI_API_KEY")
27
+ self.model = model
28
+ if AsyncOpenAI:
29
+ self.client = AsyncOpenAI(api_key=self.api_key)
30
+ else:
31
+ self.client = None
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_openai_vision_messages
37
+
38
+ return _prepare_openai_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 self.client is None:
49
+ raise RuntimeError("openai package (>=1.0.0) is not installed")
50
+
51
+ model = options.get("model", self.model)
52
+
53
+ model_info = self.MODEL_PRICING.get(model, {})
54
+ tokens_param = model_info.get("tokens_param", "max_tokens")
55
+ supports_temperature = model_info.get("supports_temperature", True)
56
+
57
+ opts = {"temperature": 1.0, "max_tokens": 512, **options}
58
+
59
+ kwargs = {
60
+ "model": model,
61
+ "messages": messages,
62
+ }
63
+ kwargs[tokens_param] = opts.get("max_tokens", 512)
64
+
65
+ if supports_temperature and "temperature" in opts:
66
+ kwargs["temperature"] = opts["temperature"]
67
+
68
+ # Native JSON mode support
69
+ if options.get("json_mode"):
70
+ json_schema = options.get("json_schema")
71
+ if json_schema:
72
+ kwargs["response_format"] = {
73
+ "type": "json_schema",
74
+ "json_schema": {
75
+ "name": "extraction",
76
+ "strict": True,
77
+ "schema": json_schema,
78
+ },
79
+ }
80
+ else:
81
+ kwargs["response_format"] = {"type": "json_object"}
82
+
83
+ resp = await self.client.chat.completions.create(**kwargs)
84
+
85
+ usage = getattr(resp, "usage", None)
86
+ prompt_tokens = getattr(usage, "prompt_tokens", 0)
87
+ completion_tokens = getattr(usage, "completion_tokens", 0)
88
+ total_tokens = getattr(usage, "total_tokens", 0)
89
+
90
+ total_cost = self._calculate_cost("openai", model, prompt_tokens, completion_tokens)
91
+
92
+ meta = {
93
+ "prompt_tokens": prompt_tokens,
94
+ "completion_tokens": completion_tokens,
95
+ "total_tokens": total_tokens,
96
+ "cost": total_cost,
97
+ "raw_response": resp.model_dump(),
98
+ "model_name": model,
99
+ }
100
+
101
+ text = resp.choices[0].message.content
102
+ return {"text": text, "meta": meta}
@@ -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": round(total_cost, 6),
96
+ "cost": total_cost,
97
97
  "raw_response": resp,
98
98
  "model_name": model,
99
99
  }