prompture 0.0.45.dev1__tar.gz → 0.0.49__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.49/.claude/skills/add-driver/SKILL.md +221 -0
- prompture-0.0.45.dev1/prompture/drivers/moonshot_driver.py → prompture-0.0.49/.claude/skills/add-driver/references/driver-template.md +88 -85
- prompture-0.0.49/.claude/skills/add-example/SKILL.md +185 -0
- prompture-0.0.49/.claude/skills/add-persona/SKILL.md +277 -0
- prompture-0.0.49/.claude/skills/add-tool/SKILL.md +222 -0
- prompture-0.0.49/.claude/skills/update-pricing/SKILL.md +136 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/PKG-INFO +35 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/README.md +34 -1
- prompture-0.0.49/VERSION +1 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/_version.py +2 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/async_conversation.py +87 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/async_groups.py +4 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/conversation.py +87 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/cost_mixin.py +1 -1
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_azure_driver.py +77 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_claude_driver.py +32 -7
- prompture-0.0.49/prompture/drivers/async_grok_driver.py +201 -0
- prompture-0.0.49/prompture/drivers/async_groq_driver.py +180 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_lmstudio_driver.py +10 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_moonshot_driver.py +89 -18
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_ollama_driver.py +111 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_openrouter_driver.py +43 -17
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/azure_driver.py +77 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/claude_driver.py +43 -7
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/grok_driver.py +101 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/groq_driver.py +92 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/lmstudio_driver.py +11 -2
- prompture-0.0.49/prompture/drivers/moonshot_driver.py +505 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/ollama_driver.py +131 -7
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/openrouter_driver.py +34 -10
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/group_types.py +1 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/groups.py +4 -0
- prompture-0.0.49/prompture/simulated_tools.py +115 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/tools_schema.py +30 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture.egg-info/PKG-INFO +35 -2
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture.egg-info/SOURCES.txt +4 -0
- prompture-0.0.45.dev1/.claude/skills/add-driver/SKILL.md +0 -85
- prompture-0.0.45.dev1/.claude/skills/add-driver/references/driver-template.md +0 -83
- prompture-0.0.45.dev1/.claude/skills/add-example/SKILL.md +0 -83
- prompture-0.0.45.dev1/.claude/skills/update-pricing/SKILL.md +0 -51
- prompture-0.0.45.dev1/prompture/drivers/async_grok_driver.py +0 -97
- prompture-0.0.45.dev1/prompture/drivers/async_groq_driver.py +0 -90
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.claude/skills/add-field/SKILL.md +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.claude/skills/add-test/SKILL.md +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.claude/skills/run-tests/SKILL.md +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.claude/skills/scaffold-extraction/SKILL.md +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.env.copy +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.github/FUNDING.yml +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.github/scripts/update_docs_version.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.github/scripts/update_wrapper_version.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.github/workflows/dev.yml +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.github/workflows/documentation.yml +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/.github/workflows/publish.yml +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/CLAUDE.md +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/LICENSE +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/MANIFEST.in +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/ROADMAP.md +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/_static/custom.css +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/_templates/footer.html +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/api/core.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/api/drivers.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/api/field_definitions.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/api/index.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/api/runner.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/api/tools.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/api/validator.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/conf.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/contributing.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/examples.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/field_definitions_reference.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/index.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/installation.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/quickstart.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/docs/source/toon_input_guide.rst +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/packages/README.md +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/packages/llm_to_json/README.md +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/packages/llm_to_json/llm_to_json/__init__.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/packages/llm_to_json/pyproject.toml +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/packages/llm_to_json/test.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/packages/llm_to_toon/README.md +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/packages/llm_to_toon/llm_to_toon/__init__.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/packages/llm_to_toon/pyproject.toml +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/packages/llm_to_toon/test.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/__init__.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/agent.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/agent_types.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/aio/__init__.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/async_agent.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/async_core.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/async_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/cache.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/callbacks.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/cli.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/core.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/discovery.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/__init__.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/airllm_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_airllm_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_google_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_hugging_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_local_http_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_modelscope_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_openai_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_registry.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/async_zai_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/google_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/hugging_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/local_http_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/modelscope_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/openai_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/registry.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/vision_helpers.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/drivers/zai_driver.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/field_definitions.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/image.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/ledger.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/logging.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/model_rates.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/persistence.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/persona.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/runner.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/scaffold/__init__.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/scaffold/generator.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/scaffold/templates/Dockerfile.j2 +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/scaffold/templates/README.md.j2 +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/scaffold/templates/config.py.j2 +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/scaffold/templates/env.example.j2 +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/scaffold/templates/main.py.j2 +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/scaffold/templates/models.py.j2 +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/scaffold/templates/requirements.txt.j2 +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/serialization.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/server.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/session.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/settings.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/tools.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture/validator.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture.egg-info/dependency_links.txt +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture.egg-info/entry_points.txt +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture.egg-info/requires.txt +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/prompture.egg-info/top_level.txt +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/pyproject.toml +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/requirements.txt +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/setup.cfg +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/test.py +0 -0
- {prompture-0.0.45.dev1 → prompture-0.0.49}/test_version_diagnosis.py +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-driver
|
|
3
|
+
description: Scaffold a new LLM provider driver for Prompture. Creates sync + async driver classes, registers them in the driver registry, adds settings, env template, setup.py extras, package exports, discovery integration, and models.dev pricing. Use when adding support for a new LLM provider.
|
|
4
|
+
metadata:
|
|
5
|
+
author: prompture
|
|
6
|
+
version: "2.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Add a New LLM Driver
|
|
10
|
+
|
|
11
|
+
Scaffolds all files needed to integrate a new LLM provider into Prompture.
|
|
12
|
+
|
|
13
|
+
## Before Starting
|
|
14
|
+
|
|
15
|
+
Ask the user for:
|
|
16
|
+
- **Provider name** (lowercase, used as registry key and `provider/model` prefix)
|
|
17
|
+
- **SDK package name** on PyPI and minimum version (or `requests`/`httpx` for raw HTTP)
|
|
18
|
+
- **Default model ID**
|
|
19
|
+
- **Authentication** — API key env var name, endpoint URL, or both
|
|
20
|
+
- **API compatibility** — OpenAI-compatible (`/v1/chat/completions`), custom SDK, or proprietary HTTP
|
|
21
|
+
- **Lazy or eager import** — lazy if SDK is optional, eager if it's in `install_requires`
|
|
22
|
+
|
|
23
|
+
Also look up the provider on [models.dev](https://models.dev) to determine:
|
|
24
|
+
- **models.dev provider name** (e.g., `"anthropic"` for Claude, `"xai"` for Grok, `"moonshotai"` for Moonshot)
|
|
25
|
+
- **Whether models.dev has entries** — if yes, pricing comes from models.dev live data (set `MODEL_PRICING = {}`). If no, add hardcoded pricing.
|
|
26
|
+
|
|
27
|
+
## Files to Create or Modify (11 total)
|
|
28
|
+
|
|
29
|
+
### 1. NEW: `prompture/drivers/{provider}_driver.py` (sync driver)
|
|
30
|
+
|
|
31
|
+
See [references/driver-template.md](references/driver-template.md) for the full skeleton.
|
|
32
|
+
|
|
33
|
+
Key rules:
|
|
34
|
+
- Subclass `CostMixin, Driver` (NOT just `Driver`)
|
|
35
|
+
- Set class-level capability flags: `supports_json_mode`, `supports_json_schema`, `supports_tool_use`, `supports_streaming`, `supports_vision`, `supports_messages`
|
|
36
|
+
- Use `self._get_model_config(provider, model)` to get per-model `tokens_param` and `supports_temperature` from models.dev
|
|
37
|
+
- Use `self._calculate_cost(provider, model, prompt_tokens, completion_tokens)` — do NOT manually compute costs
|
|
38
|
+
- Use `self._validate_model_capabilities(provider, model, ...)` before API calls to warn about unsupported features
|
|
39
|
+
- If models.dev has this provider's data, set `MODEL_PRICING = {}` (empty — pricing comes live from models.dev)
|
|
40
|
+
- `generate()` returns `{"text": str, "meta": dict}`
|
|
41
|
+
- `meta` MUST contain: `prompt_tokens`, `completion_tokens`, `total_tokens`, `cost`, `raw_response`, `model_name`
|
|
42
|
+
- Implement `generate_messages()`, `generate_messages_with_tools()`, and `generate_messages_stream()` for full feature support
|
|
43
|
+
- Optional SDK: wrap import in try/except, raise `ImportError` pointing to `pip install prompture[{provider}]`
|
|
44
|
+
|
|
45
|
+
### 2. NEW: `prompture/drivers/async_{provider}_driver.py` (async driver)
|
|
46
|
+
|
|
47
|
+
Mirror of the sync driver using `AsyncDriver` base class:
|
|
48
|
+
- Subclass `CostMixin, AsyncDriver`
|
|
49
|
+
- Same capability flags as the sync driver
|
|
50
|
+
- Share `MODEL_PRICING` from the sync driver: `MODEL_PRICING = {Provider}Driver.MODEL_PRICING`
|
|
51
|
+
- Use `httpx.AsyncClient` for HTTP calls (or async SDK methods)
|
|
52
|
+
- All generate methods are `async def`
|
|
53
|
+
- Streaming returns `AsyncIterator[dict[str, Any]]`
|
|
54
|
+
|
|
55
|
+
### 3. `prompture/drivers/__init__.py`
|
|
56
|
+
|
|
57
|
+
- Add sync import: `from .{provider}_driver import {Provider}Driver`
|
|
58
|
+
- Add async import: `from .async_{provider}_driver import Async{Provider}Driver`
|
|
59
|
+
- Register sync driver with `register_driver()`:
|
|
60
|
+
```python
|
|
61
|
+
register_driver(
|
|
62
|
+
"{provider}",
|
|
63
|
+
lambda model=None: {Provider}Driver(
|
|
64
|
+
api_key=settings.{provider}_api_key,
|
|
65
|
+
model=model or settings.{provider}_model,
|
|
66
|
+
),
|
|
67
|
+
overwrite=True,
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
- Add `"{Provider}Driver"` and `"Async{Provider}Driver"` to `__all__`
|
|
71
|
+
|
|
72
|
+
### 4. `prompture/__init__.py`
|
|
73
|
+
|
|
74
|
+
- Add `{Provider}Driver` to the `.drivers` import line
|
|
75
|
+
- Add `"{Provider}Driver"` to `__all__` under `# Drivers`
|
|
76
|
+
|
|
77
|
+
### 5. `prompture/settings.py`
|
|
78
|
+
|
|
79
|
+
Add inside `Settings` class:
|
|
80
|
+
```python
|
|
81
|
+
# {Provider}
|
|
82
|
+
{provider}_api_key: Optional[str] = None
|
|
83
|
+
{provider}_model: str = "default-model"
|
|
84
|
+
# Add endpoint if the provider supports custom endpoints:
|
|
85
|
+
# {provider}_endpoint: str = "https://api.example.com/v1"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 6. `prompture/discovery.py`
|
|
89
|
+
|
|
90
|
+
Two changes required:
|
|
91
|
+
|
|
92
|
+
**a) Add to `provider_classes` dict and configuration check:**
|
|
93
|
+
- Import the driver class at the top of the file
|
|
94
|
+
- Add to `provider_classes`: `"{provider}": {Provider}Driver`
|
|
95
|
+
- Add configuration check in the `is_configured` block:
|
|
96
|
+
```python
|
|
97
|
+
elif provider == "{provider}":
|
|
98
|
+
if settings.{provider}_api_key or os.getenv("{PROVIDER}_API_KEY"):
|
|
99
|
+
is_configured = True
|
|
100
|
+
```
|
|
101
|
+
For local/endpoint-only providers (like ollama), use endpoint presence instead.
|
|
102
|
+
|
|
103
|
+
**b) This ensures `get_available_models()` returns the provider's models** from both:
|
|
104
|
+
- Static detection: `MODEL_PRICING` keys (or empty if pricing is from models.dev)
|
|
105
|
+
- models.dev enrichment: via `PROVIDER_MAP` in `model_rates.py` (see step 7)
|
|
106
|
+
|
|
107
|
+
### 7. `prompture/model_rates.py` — `PROVIDER_MAP`
|
|
108
|
+
|
|
109
|
+
If models.dev has this provider's data, add the mapping:
|
|
110
|
+
```python
|
|
111
|
+
PROVIDER_MAP: dict[str, str] = {
|
|
112
|
+
...
|
|
113
|
+
"{provider}": "{models_dev_name}", # e.g., "moonshot": "moonshotai"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This enables:
|
|
118
|
+
- **Live pricing** via `get_model_rates()` — used by `CostMixin._calculate_cost()`
|
|
119
|
+
- **Capability metadata** via `get_model_capabilities()` — used by `_get_model_config()` and `_validate_model_capabilities()`
|
|
120
|
+
- **Model discovery** via `get_all_provider_models()` — called by `discovery.py` to list all available models
|
|
121
|
+
|
|
122
|
+
To find the correct models.dev name, check: `https://models.dev/{models_dev_name}`
|
|
123
|
+
|
|
124
|
+
If models.dev does NOT have this provider, skip this step. The driver will use hardcoded `MODEL_PRICING` for costs and return `None` for capabilities.
|
|
125
|
+
|
|
126
|
+
### 8. `setup.py` / `pyproject.toml`
|
|
127
|
+
|
|
128
|
+
If optional: add `"{provider}": ["{sdk}>={version}"]` to `extras_require`.
|
|
129
|
+
If required: add to `install_requires`.
|
|
130
|
+
|
|
131
|
+
### 9. `.env.copy`
|
|
132
|
+
|
|
133
|
+
Add section:
|
|
134
|
+
```
|
|
135
|
+
# {Provider} Configuration
|
|
136
|
+
{PROVIDER}_API_KEY=your-api-key-here
|
|
137
|
+
{PROVIDER}_MODEL=default-model
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 10. `CLAUDE.md`
|
|
141
|
+
|
|
142
|
+
Add `{provider}` to the driver list in the Module Layout bullet.
|
|
143
|
+
|
|
144
|
+
### 11. OPTIONAL: `examples/{provider}_example.py`
|
|
145
|
+
|
|
146
|
+
Follow the existing example pattern (see `grok_example.py` or `groq_example.py`):
|
|
147
|
+
- Two extraction examples: default instruction + custom instruction
|
|
148
|
+
- Show different models if available
|
|
149
|
+
- Print JSON output and token usage statistics
|
|
150
|
+
|
|
151
|
+
## Important: Reasoning Model Handling
|
|
152
|
+
|
|
153
|
+
If the provider has reasoning models (models with `reasoning: true` on models.dev):
|
|
154
|
+
- Check `caps.is_reasoning` before sending `response_format` — reasoning models often don't support it
|
|
155
|
+
- Handle `reasoning_content` field in responses (both regular and streaming)
|
|
156
|
+
- Some reasoning models don't support `temperature` — respect `supports_temperature` from `_get_model_config()`
|
|
157
|
+
|
|
158
|
+
Example pattern (see `moonshot_driver.py`):
|
|
159
|
+
```python
|
|
160
|
+
if options.get("json_mode"):
|
|
161
|
+
from ..model_rates import get_model_capabilities
|
|
162
|
+
|
|
163
|
+
caps = get_model_capabilities("{provider}", model)
|
|
164
|
+
is_reasoning = caps is not None and caps.is_reasoning is True
|
|
165
|
+
model_supports_structured = (
|
|
166
|
+
caps is None or caps.supports_structured_output is not False
|
|
167
|
+
) and not is_reasoning
|
|
168
|
+
|
|
169
|
+
if model_supports_structured:
|
|
170
|
+
# Send response_format
|
|
171
|
+
...
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## How models.dev Integration Works
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
User calls extract_and_jsonify("moonshot/kimi-k2.5", ...)
|
|
178
|
+
│
|
|
179
|
+
├─► core.py checks driver.supports_json_mode → decides json_mode
|
|
180
|
+
│
|
|
181
|
+
├─► driver._get_model_config("moonshot", "kimi-k2.5")
|
|
182
|
+
│ └─► model_rates.get_model_capabilities("moonshot", "kimi-k2.5")
|
|
183
|
+
│ └─► PROVIDER_MAP["moonshot"] → "moonshotai"
|
|
184
|
+
│ └─► models.dev data["moonshotai"]["models"]["kimi-k2.5"]
|
|
185
|
+
│ └─► Returns: supports_temperature, is_reasoning, context_window, etc.
|
|
186
|
+
│
|
|
187
|
+
├─► driver._calculate_cost("moonshot", "kimi-k2.5", tokens...)
|
|
188
|
+
│ └─► model_rates.get_model_rates("moonshot", "kimi-k2.5")
|
|
189
|
+
│ └─► Same lookup → returns {input: 0.6, output: 3.0} per 1M tokens
|
|
190
|
+
│
|
|
191
|
+
└─► discovery.get_available_models()
|
|
192
|
+
└─► Iterates PROVIDER_MAP → get_all_provider_models("moonshotai")
|
|
193
|
+
└─► Returns all model IDs under the provider
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Model Name Resolution
|
|
197
|
+
|
|
198
|
+
Model names are **always provider-scoped**. The format is `"provider/model_id"`.
|
|
199
|
+
|
|
200
|
+
- `get_driver_for_model("openrouter/qwen-2.5")` → looks up `"openrouter"` in the driver registry
|
|
201
|
+
- `get_model_capabilities("openrouter", "qwen-2.5")` → looks in models.dev under `data["openrouter"]["models"]["qwen-2.5"]`
|
|
202
|
+
- `get_model_capabilities("modelscope", "qwen-2.5")` → looks in models.dev under `data["modelscope"]["models"]["qwen-2.5"]`
|
|
203
|
+
|
|
204
|
+
The same model ID under different providers is **not ambiguous** — each provider has its own namespace in both the driver registry and models.dev data.
|
|
205
|
+
|
|
206
|
+
## Verification
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Import check
|
|
210
|
+
python -c "from prompture import {Provider}Driver; print('OK')"
|
|
211
|
+
python -c "from prompture.drivers import Async{Provider}Driver; print('OK')"
|
|
212
|
+
|
|
213
|
+
# Registry check
|
|
214
|
+
python -c "from prompture.drivers import get_driver_for_model; d = get_driver_for_model('{provider}/test'); print(type(d).__name__, d.model)"
|
|
215
|
+
|
|
216
|
+
# Discovery check
|
|
217
|
+
python -c "from prompture import get_available_models; ms = [m for m in get_available_models() if m.startswith('{provider}/')]; print(f'Found {{len(ms)}} models'); print(ms[:5])"
|
|
218
|
+
|
|
219
|
+
# Run tests
|
|
220
|
+
pytest tests/ -x -q
|
|
221
|
+
```
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
Requires the `requests` package. Uses MOONSHOT_API_KEY env var.
|
|
1
|
+
# Driver Template
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Every Prompture driver follows this skeleton. The sync driver uses `requests`,
|
|
4
|
+
the async driver uses `httpx`.
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
## Sync Driver — `prompture/drivers/{provider}_driver.py`
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
"""{Provider} driver implementation.
|
|
10
|
+
Requires the `requests` package. Uses {PROVIDER}_API_KEY env var.
|
|
11
|
+
|
|
12
|
+
All pricing comes from models.dev (provider: "{models_dev_name}") — no hardcoded pricing.
|
|
10
13
|
"""
|
|
11
14
|
|
|
12
15
|
import json
|
|
16
|
+
import logging
|
|
13
17
|
import os
|
|
14
18
|
from collections.abc import Iterator
|
|
15
19
|
from typing import Any
|
|
@@ -19,89 +23,64 @@ import requests
|
|
|
19
23
|
from ..cost_mixin import CostMixin, prepare_strict_schema
|
|
20
24
|
from ..driver import Driver
|
|
21
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
22
28
|
|
|
23
|
-
class
|
|
29
|
+
class {Provider}Driver(CostMixin, Driver):
|
|
24
30
|
supports_json_mode = True
|
|
25
31
|
supports_json_schema = True
|
|
26
32
|
supports_tool_use = True
|
|
27
33
|
supports_streaming = True
|
|
28
|
-
supports_vision = True
|
|
34
|
+
supports_vision = False # set True if the provider supports image input
|
|
35
|
+
supports_messages = True
|
|
29
36
|
|
|
30
|
-
# All pricing resolved live from models.dev (provider: "
|
|
37
|
+
# All pricing resolved live from models.dev (provider: "{models_dev_name}")
|
|
38
|
+
# If models.dev does NOT have this provider, add hardcoded pricing:
|
|
39
|
+
# MODEL_PRICING = {
|
|
40
|
+
# "model-name": {"prompt": 0.001, "completion": 0.002},
|
|
41
|
+
# }
|
|
31
42
|
MODEL_PRICING: dict[str, dict[str, Any]] = {}
|
|
32
43
|
|
|
33
44
|
def __init__(
|
|
34
45
|
self,
|
|
35
46
|
api_key: str | None = None,
|
|
36
|
-
model: str = "
|
|
37
|
-
endpoint: str = "https://api.
|
|
47
|
+
model: str = "default-model",
|
|
48
|
+
endpoint: str = "https://api.example.com/v1",
|
|
38
49
|
):
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
api_key: Moonshot API key. If not provided, will look for MOONSHOT_API_KEY env var.
|
|
43
|
-
model: Model to use. Defaults to kimi-k2-0905-preview.
|
|
44
|
-
endpoint: API base URL. Defaults to https://api.moonshot.ai/v1.
|
|
45
|
-
Use https://api.moonshot.cn/v1 for the China endpoint.
|
|
46
|
-
"""
|
|
47
|
-
self.api_key = api_key or os.getenv("MOONSHOT_API_KEY")
|
|
50
|
+
self.api_key = api_key or os.getenv("{PROVIDER}_API_KEY")
|
|
48
51
|
if not self.api_key:
|
|
49
|
-
raise ValueError("
|
|
52
|
+
raise ValueError("{Provider} API key not found. Set {PROVIDER}_API_KEY env var.")
|
|
50
53
|
|
|
51
54
|
self.model = model
|
|
52
55
|
self.base_url = endpoint.rstrip("/")
|
|
53
|
-
|
|
54
56
|
self.headers = {
|
|
55
57
|
"Authorization": f"Bearer {self.api_key}",
|
|
56
58
|
"Content-Type": "application/json",
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
supports_messages = True
|
|
60
|
-
|
|
61
|
-
@staticmethod
|
|
62
|
-
def _clamp_temperature(opts: dict[str, Any]) -> dict[str, Any]:
|
|
63
|
-
"""Clamp temperature to Moonshot's supported range [0, 1]."""
|
|
64
|
-
if "temperature" in opts:
|
|
65
|
-
opts["temperature"] = max(0.0, min(1.0, float(opts["temperature"])))
|
|
66
|
-
return opts
|
|
67
|
-
|
|
68
|
-
@staticmethod
|
|
69
|
-
def _sanitize_tool_choice(data: dict[str, Any]) -> dict[str, Any]:
|
|
70
|
-
"""Downgrade tool_choice='required' to 'auto' (unsupported by Moonshot)."""
|
|
71
|
-
if data.get("tool_choice") == "required":
|
|
72
|
-
data["tool_choice"] = "auto"
|
|
73
|
-
return data
|
|
74
|
-
|
|
75
|
-
def _prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
76
|
-
from .vision_helpers import _prepare_openai_vision_messages
|
|
77
|
-
|
|
78
|
-
return _prepare_openai_vision_messages(messages)
|
|
79
|
-
|
|
80
61
|
def generate(self, prompt: str, options: dict[str, Any]) -> dict[str, Any]:
|
|
81
62
|
messages = [{"role": "user", "content": prompt}]
|
|
82
63
|
return self._do_generate(messages, options)
|
|
83
64
|
|
|
84
65
|
def generate_messages(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
|
|
85
|
-
return self._do_generate(
|
|
66
|
+
return self._do_generate(messages, options)
|
|
86
67
|
|
|
87
68
|
def _do_generate(self, messages: list[dict[str, str]], options: dict[str, Any]) -> dict[str, Any]:
|
|
88
|
-
if not self.api_key:
|
|
89
|
-
raise RuntimeError("Moonshot API key not found")
|
|
90
|
-
|
|
91
69
|
model = options.get("model", self.model)
|
|
92
70
|
|
|
93
|
-
|
|
71
|
+
# Per-model config from models.dev (tokens_param, supports_temperature, etc.)
|
|
72
|
+
model_config = self._get_model_config("{provider}", model)
|
|
94
73
|
tokens_param = model_config["tokens_param"]
|
|
95
74
|
supports_temperature = model_config["supports_temperature"]
|
|
96
75
|
|
|
76
|
+
# Validate capabilities (logs warnings if model doesn't support requested features)
|
|
97
77
|
self._validate_model_capabilities(
|
|
98
|
-
"
|
|
78
|
+
"{provider}",
|
|
99
79
|
model,
|
|
100
80
|
using_json_schema=bool(options.get("json_schema")),
|
|
101
81
|
)
|
|
102
82
|
|
|
103
83
|
opts = {"temperature": 1.0, "max_tokens": 512, **options}
|
|
104
|
-
opts = self._clamp_temperature(opts)
|
|
105
84
|
|
|
106
85
|
data: dict[str, Any] = {
|
|
107
86
|
"model": model,
|
|
@@ -112,12 +91,11 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
112
91
|
if supports_temperature and "temperature" in opts:
|
|
113
92
|
data["temperature"] = opts["temperature"]
|
|
114
93
|
|
|
115
|
-
# Native JSON mode
|
|
116
|
-
# Moonshot's API does not reliably support response_format.
|
|
94
|
+
# Native JSON mode — check per-model capabilities before sending response_format
|
|
117
95
|
if options.get("json_mode"):
|
|
118
96
|
from ..model_rates import get_model_capabilities
|
|
119
97
|
|
|
120
|
-
caps = get_model_capabilities("
|
|
98
|
+
caps = get_model_capabilities("{provider}", model)
|
|
121
99
|
is_reasoning = caps is not None and caps.is_reasoning is True
|
|
122
100
|
model_supports_structured = (
|
|
123
101
|
caps is None or caps.supports_structured_output is not False
|
|
@@ -148,17 +126,17 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
148
126
|
response.raise_for_status()
|
|
149
127
|
resp = response.json()
|
|
150
128
|
except requests.exceptions.HTTPError as e:
|
|
151
|
-
|
|
152
|
-
raise RuntimeError(error_msg) from e
|
|
129
|
+
raise RuntimeError(f"{Provider} API request failed: {e!s}") from e
|
|
153
130
|
except requests.exceptions.RequestException as e:
|
|
154
|
-
raise RuntimeError(f"
|
|
131
|
+
raise RuntimeError(f"{Provider} API request failed: {e!s}") from e
|
|
155
132
|
|
|
156
133
|
usage = resp.get("usage", {})
|
|
157
134
|
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
158
135
|
completion_tokens = usage.get("completion_tokens", 0)
|
|
159
136
|
total_tokens = usage.get("total_tokens", 0)
|
|
160
137
|
|
|
161
|
-
|
|
138
|
+
# Cost calculated from models.dev live rates, falling back to MODEL_PRICING
|
|
139
|
+
total_cost = self._calculate_cost("{provider}", model, prompt_tokens, completion_tokens)
|
|
162
140
|
|
|
163
141
|
meta = {
|
|
164
142
|
"prompt_tokens": prompt_tokens,
|
|
@@ -189,18 +167,14 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
189
167
|
options: dict[str, Any],
|
|
190
168
|
) -> dict[str, Any]:
|
|
191
169
|
"""Generate a response that may include tool calls."""
|
|
192
|
-
if not self.api_key:
|
|
193
|
-
raise RuntimeError("Moonshot API key not found")
|
|
194
|
-
|
|
195
170
|
model = options.get("model", self.model)
|
|
196
|
-
model_config = self._get_model_config("
|
|
171
|
+
model_config = self._get_model_config("{provider}", model)
|
|
197
172
|
tokens_param = model_config["tokens_param"]
|
|
198
173
|
supports_temperature = model_config["supports_temperature"]
|
|
199
174
|
|
|
200
|
-
self._validate_model_capabilities("
|
|
175
|
+
self._validate_model_capabilities("{provider}", model, using_tool_use=True)
|
|
201
176
|
|
|
202
177
|
opts = {"temperature": 1.0, "max_tokens": 512, **options}
|
|
203
|
-
opts = self._clamp_temperature(opts)
|
|
204
178
|
|
|
205
179
|
data: dict[str, Any] = {
|
|
206
180
|
"model": model,
|
|
@@ -215,8 +189,6 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
215
189
|
if "tool_choice" in options:
|
|
216
190
|
data["tool_choice"] = options["tool_choice"]
|
|
217
191
|
|
|
218
|
-
data = self._sanitize_tool_choice(data)
|
|
219
|
-
|
|
220
192
|
try:
|
|
221
193
|
response = requests.post(
|
|
222
194
|
f"{self.base_url}/chat/completions",
|
|
@@ -227,16 +199,15 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
227
199
|
response.raise_for_status()
|
|
228
200
|
resp = response.json()
|
|
229
201
|
except requests.exceptions.HTTPError as e:
|
|
230
|
-
|
|
231
|
-
raise RuntimeError(error_msg) from e
|
|
202
|
+
raise RuntimeError(f"{Provider} API request failed: {e!s}") from e
|
|
232
203
|
except requests.exceptions.RequestException as e:
|
|
233
|
-
raise RuntimeError(f"
|
|
204
|
+
raise RuntimeError(f"{Provider} API request failed: {e!s}") from e
|
|
234
205
|
|
|
235
206
|
usage = resp.get("usage", {})
|
|
236
207
|
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
237
208
|
completion_tokens = usage.get("completion_tokens", 0)
|
|
238
209
|
total_tokens = usage.get("total_tokens", 0)
|
|
239
|
-
total_cost = self._calculate_cost("
|
|
210
|
+
total_cost = self._calculate_cost("{provider}", model, prompt_tokens, completion_tokens)
|
|
240
211
|
|
|
241
212
|
meta = {
|
|
242
213
|
"prompt_tokens": prompt_tokens,
|
|
@@ -257,13 +228,11 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
257
228
|
args = json.loads(tc["function"]["arguments"])
|
|
258
229
|
except (json.JSONDecodeError, TypeError):
|
|
259
230
|
args = {}
|
|
260
|
-
tool_calls_out.append(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
)
|
|
231
|
+
tool_calls_out.append({
|
|
232
|
+
"id": tc["id"],
|
|
233
|
+
"name": tc["function"]["name"],
|
|
234
|
+
"arguments": args,
|
|
235
|
+
})
|
|
267
236
|
|
|
268
237
|
return {
|
|
269
238
|
"text": text,
|
|
@@ -281,17 +250,13 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
281
250
|
messages: list[dict[str, Any]],
|
|
282
251
|
options: dict[str, Any],
|
|
283
252
|
) -> Iterator[dict[str, Any]]:
|
|
284
|
-
"""Yield response chunks via
|
|
285
|
-
if not self.api_key:
|
|
286
|
-
raise RuntimeError("Moonshot API key not found")
|
|
287
|
-
|
|
253
|
+
"""Yield response chunks via streaming API."""
|
|
288
254
|
model = options.get("model", self.model)
|
|
289
|
-
model_config = self._get_model_config("
|
|
255
|
+
model_config = self._get_model_config("{provider}", model)
|
|
290
256
|
tokens_param = model_config["tokens_param"]
|
|
291
257
|
supports_temperature = model_config["supports_temperature"]
|
|
292
258
|
|
|
293
259
|
opts = {"temperature": 1.0, "max_tokens": 512, **options}
|
|
294
|
-
opts = self._clamp_temperature(opts)
|
|
295
260
|
|
|
296
261
|
data: dict[str, Any] = {
|
|
297
262
|
"model": model,
|
|
@@ -320,7 +285,7 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
320
285
|
for line in response.iter_lines(decode_unicode=True):
|
|
321
286
|
if not line or not line.startswith("data: "):
|
|
322
287
|
continue
|
|
323
|
-
payload = line[len("data: ")
|
|
288
|
+
payload = line[len("data: "):]
|
|
324
289
|
if payload.strip() == "[DONE]":
|
|
325
290
|
break
|
|
326
291
|
try:
|
|
@@ -345,7 +310,7 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
345
310
|
yield {"type": "delta", "text": content}
|
|
346
311
|
|
|
347
312
|
total_tokens = prompt_tokens + completion_tokens
|
|
348
|
-
total_cost = self._calculate_cost("
|
|
313
|
+
total_cost = self._calculate_cost("{provider}", model, prompt_tokens, completion_tokens)
|
|
349
314
|
|
|
350
315
|
yield {
|
|
351
316
|
"type": "done",
|
|
@@ -359,3 +324,41 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
359
324
|
"model_name": model,
|
|
360
325
|
},
|
|
361
326
|
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Lazy Import Pattern (for optional SDKs)
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
def __init__(self, ...):
|
|
333
|
+
self._client = None
|
|
334
|
+
# defer import
|
|
335
|
+
|
|
336
|
+
def _ensure_client(self):
|
|
337
|
+
if self._client is not None:
|
|
338
|
+
return
|
|
339
|
+
try:
|
|
340
|
+
from some_sdk import Client
|
|
341
|
+
except ImportError:
|
|
342
|
+
raise ImportError(
|
|
343
|
+
"The 'some-sdk' package is required. "
|
|
344
|
+
"Install with: pip install prompture[provider]"
|
|
345
|
+
)
|
|
346
|
+
self._client = Client(api_key=self.api_key)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Existing Drivers for Reference
|
|
350
|
+
|
|
351
|
+
| Driver | File | SDK | Auth | models.dev |
|
|
352
|
+
|--------|------|-----|------|------------|
|
|
353
|
+
| OpenAI | `openai_driver.py` | `openai` | API key | `openai` |
|
|
354
|
+
| Claude | `claude_driver.py` | `anthropic` | API key | `anthropic` |
|
|
355
|
+
| Google | `google_driver.py` | `google-generativeai` | API key | `google` |
|
|
356
|
+
| Groq | `groq_driver.py` | `groq` | API key | `groq` |
|
|
357
|
+
| Grok | `grok_driver.py` | `requests` | API key | `xai` |
|
|
358
|
+
| Moonshot | `moonshot_driver.py` | `requests` | API key + endpoint | `moonshotai` |
|
|
359
|
+
| Z.ai | `zai_driver.py` | `requests` | API key + endpoint | `zai` |
|
|
360
|
+
| ModelScope | `modelscope_driver.py` | `requests` | API key + endpoint | — |
|
|
361
|
+
| OpenRouter | `openrouter_driver.py` | `requests` | API key | `openrouter` |
|
|
362
|
+
| Ollama | `ollama_driver.py` | `requests` | Endpoint URL | — |
|
|
363
|
+
| LM Studio | `lmstudio_driver.py` | `requests` | Endpoint URL | — |
|
|
364
|
+
| AirLLM | `airllm_driver.py` | `airllm` (lazy) | None (local) | — |
|