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.
- {prompture-0.0.38.dev2/prompture.egg-info → prompture-0.0.38.dev3}/PKG-INFO +1 -1
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/_version.py +2 -2
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_azure_driver.py +1 -1
- prompture-0.0.38.dev3/prompture/drivers/async_claude_driver.py +272 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_grok_driver.py +1 -1
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_groq_driver.py +1 -1
- prompture-0.0.38.dev3/prompture/drivers/async_openai_driver.py +244 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_openrouter_driver.py +1 -1
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3/prompture.egg-info}/PKG-INFO +1 -1
- prompture-0.0.38.dev2/prompture/drivers/async_claude_driver.py +0 -113
- prompture-0.0.38.dev2/prompture/drivers/async_openai_driver.py +0 -102
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-driver/SKILL.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-driver/references/driver-template.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-example/SKILL.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-field/SKILL.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/add-test/SKILL.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/run-tests/SKILL.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/scaffold-extraction/SKILL.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.claude/skills/update-pricing/SKILL.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.env.copy +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/FUNDING.yml +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/scripts/update_docs_version.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/scripts/update_wrapper_version.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/workflows/dev.yml +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/workflows/documentation.yml +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/.github/workflows/publish.yml +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/CLAUDE.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/LICENSE +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/MANIFEST.in +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/README.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/ROADMAP.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/_static/custom.css +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/_templates/footer.html +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/core.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/drivers.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/field_definitions.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/index.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/runner.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/tools.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/api/validator.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/conf.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/contributing.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/examples.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/field_definitions_reference.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/index.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/installation.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/quickstart.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/docs/source/toon_input_guide.rst +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/README.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_json/README.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_json/llm_to_json/__init__.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_json/pyproject.toml +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_json/test.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_toon/README.md +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_toon/llm_to_toon/__init__.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_toon/pyproject.toml +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/packages/llm_to_toon/test.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/__init__.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/agent.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/agent_types.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/aio/__init__.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_agent.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_conversation.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_core.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/async_groups.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/cache.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/callbacks.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/cli.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/conversation.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/core.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/cost_mixin.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/discovery.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/__init__.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/airllm_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_airllm_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_google_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_hugging_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_lmstudio_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_local_http_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_ollama_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_registry.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/azure_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/claude_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/google_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/grok_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/groq_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/hugging_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/lmstudio_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/local_http_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/ollama_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/openai_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/openrouter_driver.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/registry.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/vision_helpers.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/field_definitions.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/group_types.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/groups.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/image.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/logging.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/model_rates.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/persistence.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/persona.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/runner.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/__init__.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/generator.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/Dockerfile.j2 +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/README.md.j2 +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/config.py.j2 +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/env.example.j2 +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/main.py.j2 +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/models.py.j2 +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/scaffold/templates/requirements.txt.j2 +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/serialization.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/server.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/session.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/settings.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/tools.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/tools_schema.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/validator.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/SOURCES.txt +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/dependency_links.txt +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/entry_points.txt +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/requires.txt +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture.egg-info/top_level.txt +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/pyproject.toml +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/requirements.txt +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/setup.cfg +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/test.py +0 -0
- {prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/test_version_diagnosis.py +0 -0
|
@@ -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.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
|
+
}
|
{prompture-0.0.38.dev2 → prompture-0.0.38.dev3}/prompture/drivers/async_openrouter_driver.py
RENAMED
|
@@ -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
|
}
|