openaivec 1.0.9__tar.gz → 1.0.11__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.
- openaivec-1.0.11/.github/workflows/test-pr.yml +71 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/.github/workflows/test.yml +1 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/PKG-INFO +9 -8
- {openaivec-1.0.9 → openaivec-1.0.11}/README.md +8 -7
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_cache/optimize.py +20 -5
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_provider.py +57 -6
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/_cache/test_optimize.py +78 -6
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/_cache/test_proxy_suggester.py +2 -1
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_provider.py +103 -15
- {openaivec-1.0.9 → openaivec-1.0.11}/uv.lock +237 -237
- {openaivec-1.0.9 → openaivec-1.0.11}/.env.example +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/.github/copilot-instructions.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/.github/dependabot.yml +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/.github/workflows/docs.yml +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/.github/workflows/publish.yml +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/.gitignore +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/AGENTS.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/CODE_OF_CONDUCT.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/LICENSE +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/SECURITY.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/SUPPORT.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/main.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/pandas_ext.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/spark.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/task.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/customer_support/customer_sentiment.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/customer_support/inquiry_classification.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/customer_support/inquiry_summary.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/customer_support/intent_analysis.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/customer_support/response_suggestion.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/customer_support/urgency_analysis.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/nlp/dependency_parsing.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/nlp/keyword_extraction.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/nlp/morphological_analysis.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/nlp/named_entity_recognition.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/nlp/sentiment_analysis.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/api/tasks/nlp/translation.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/contributor-guide.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/index.md +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/overrides/main.html +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/docs/robots.txt +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/mkdocs.yml +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/pyproject.toml +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/pytest.ini +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/__init__.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_cache/__init__.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_cache/proxy.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_di.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_embeddings.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_log.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_model.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_prompt.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_responses.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_schema/__init__.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_schema/infer.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_schema/spec.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_serialize.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/_util.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/pandas_ext.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/spark.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/__init__.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/customer_support/__init__.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/customer_support/customer_sentiment.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/customer_support/inquiry_classification.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/customer_support/inquiry_summary.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/customer_support/intent_analysis.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/customer_support/response_suggestion.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/customer_support/urgency_analysis.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/nlp/__init__.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/nlp/dependency_parsing.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/nlp/keyword_extraction.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/nlp/morphological_analysis.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/nlp/named_entity_recognition.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/nlp/sentiment_analysis.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/nlp/translation.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/table/__init__.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/src/openaivec/task/table/fillna.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/__init__.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/_cache/test_proxy.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/_schema/test_infer.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/_schema/test_spec.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/conftest.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_di.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_embeddings.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_pandas_ext.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_prompt.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_responses.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_serialize.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_serialize_pydantic_v2_compliance.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_spark.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_task.py +0 -0
- {openaivec-1.0.9 → openaivec-1.0.11}/tests/test_util.py +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: ci-integration
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
issues: write
|
|
10
|
+
pull-requests: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
if: >
|
|
15
|
+
github.event.issue.pull_request &&
|
|
16
|
+
contains(github.event.comment.body, '/run-integration') &&
|
|
17
|
+
(github.event.comment.author_association == 'MEMBER' ||
|
|
18
|
+
github.event.comment.author_association == 'OWNER' ||
|
|
19
|
+
github.event.comment.author_association == 'COLLABORATOR')
|
|
20
|
+
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment: integration
|
|
23
|
+
env:
|
|
24
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- name: Comment workflow run link
|
|
28
|
+
shell: bash
|
|
29
|
+
run: |
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
COMMENTS_URL="${{ github.event.issue.comments_url }}"
|
|
32
|
+
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
33
|
+
export RUN_URL
|
|
34
|
+
PAYLOAD="$(python -c 'import json,os; print(json.dumps({\"body\": f\"Integration workflow started: {os.environ[\\\"RUN_URL\\\"]}\"}))')"
|
|
35
|
+
curl -sS -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
|
36
|
+
-H "Accept: application/vnd.github+json" \
|
|
37
|
+
-d "$PAYLOAD" \
|
|
38
|
+
"$COMMENTS_URL"
|
|
39
|
+
|
|
40
|
+
- name: Fetch PR head SHA
|
|
41
|
+
id: pr
|
|
42
|
+
shell: bash
|
|
43
|
+
run: |
|
|
44
|
+
set -euo pipefail
|
|
45
|
+
PR_API_URL="${{ github.event.issue.pull_request.url }}"
|
|
46
|
+
JSON="$(curl -sS -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
|
47
|
+
-H "Accept: application/vnd.github+json" "$PR_API_URL")"
|
|
48
|
+
echo "sha=$(python -c 'import json,sys; print(json.load(sys.stdin)[\"head\"][\"sha\"])' <<<\"$JSON\")" >> "$GITHUB_OUTPUT"
|
|
49
|
+
|
|
50
|
+
- name: Checkout PR head commit
|
|
51
|
+
uses: actions/checkout@v4
|
|
52
|
+
with:
|
|
53
|
+
ref: ${{ steps.pr.outputs.sha }}
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@v7
|
|
57
|
+
|
|
58
|
+
- name: Set up Python
|
|
59
|
+
run: uv python install 3.10
|
|
60
|
+
|
|
61
|
+
- name: Install dependencies via uv
|
|
62
|
+
run: uv sync --all-extras --dev
|
|
63
|
+
|
|
64
|
+
- name: Lint with ruff
|
|
65
|
+
run: uv run ruff check .
|
|
66
|
+
|
|
67
|
+
- name: Type check with pyright
|
|
68
|
+
run: uv run pyright src/openaivec || echo "Type check completed with issues - see above"
|
|
69
|
+
|
|
70
|
+
- name: Run tests
|
|
71
|
+
run: uv run pytest
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openaivec
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.11
|
|
4
4
|
Summary: Generative mutation for tabular calculation
|
|
5
5
|
Project-URL: Homepage, https://microsoft.github.io/openaivec/
|
|
6
6
|
Project-URL: Repository, https://github.com/microsoft/openaivec
|
|
@@ -60,9 +60,10 @@ sentiment = reviews.ai.responses(
|
|
|
60
60
|
reasoning={"effort": "none"}, # Mirrors OpenAI SDK for reasoning models
|
|
61
61
|
)
|
|
62
62
|
print(sentiment.tolist())
|
|
63
|
+
# Output: ['Positive sentiment', 'Negative sentiment']
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
**
|
|
66
|
+
**Pandas tutorial (GitHub Pages):** https://microsoft.github.io/openaivec/examples/pandas/
|
|
66
67
|
|
|
67
68
|
## Benchmarks
|
|
68
69
|
|
|
@@ -81,6 +82,7 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
|
|
|
81
82
|
## Contents
|
|
82
83
|
|
|
83
84
|
- [Why openaivec?](#why-openaivec)
|
|
85
|
+
- [Overview](#overview)
|
|
84
86
|
- [Core Workflows](#core-workflows)
|
|
85
87
|
- [Using with Apache Spark UDFs](#using-with-apache-spark-udfs)
|
|
86
88
|
- [Building Prompts](#building-prompts)
|
|
@@ -92,14 +94,13 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
|
|
|
92
94
|
## Why openaivec?
|
|
93
95
|
|
|
94
96
|
- Drop-in `.ai` and `.aio` accessors keep pandas analysts in familiar tooling.
|
|
95
|
-
- OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, and
|
|
96
|
-
- Smart batching (`BatchingMapProxy`/`AsyncBatchingMapProxy`) dedupes prompts, preserves order, and releases waiters on failure.
|
|
97
|
+
- OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, preserve order, and release waiters on failure.
|
|
97
98
|
- Reasoning support mirrors the OpenAI SDK; structured outputs accept Pydantic `response_format`.
|
|
98
99
|
- Built-in caches and retries remove boilerplate; helpers reuse caches across pandas, Spark, and async flows.
|
|
99
100
|
- Spark UDFs and Microsoft Fabric guides move notebooks into production-scale ETL.
|
|
100
101
|
- Prompt tooling (`FewShotPromptBuilder`, `improve`) and the task library ship curated prompts with validated outputs.
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
## Overview
|
|
103
104
|
|
|
104
105
|
Vectorized OpenAI batch processing so you handle many inputs per call instead of one-by-one. Batching proxies dedupe inputs, enforce ordered outputs, and unblock waiters even on upstream errors. Cache helpers (`responses_with_cache`, Spark UDF builders) plug into the same layer so expensive prompts are reused across pandas, Spark, and async flows. Reasoning models honor SDK semantics. Requires Python 3.10+.
|
|
105
106
|
|
|
@@ -185,7 +186,7 @@ result = df.assign(
|
|
|
185
186
|
|
|
186
187
|
### Using with reasoning models
|
|
187
188
|
|
|
188
|
-
Reasoning models (o1-preview, o1-mini, o3-mini, etc.)
|
|
189
|
+
Reasoning models (o1-preview, o1-mini, o3-mini, etc.) follow OpenAI SDK semantics. Pass `reasoning` when you want to override model defaults.
|
|
189
190
|
|
|
190
191
|
```python
|
|
191
192
|
pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
|
|
@@ -193,7 +194,7 @@ pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
|
|
|
193
194
|
result = df.assign(
|
|
194
195
|
analysis=lambda df: df.text.ai.responses(
|
|
195
196
|
"Analyze this text step by step",
|
|
196
|
-
reasoning={"effort": "none"} # Optional: mirrors the OpenAI SDK argument
|
|
197
|
+
reasoning={"effort": "none"}, # Optional: mirrors the OpenAI SDK argument
|
|
197
198
|
)
|
|
198
199
|
)
|
|
199
200
|
```
|
|
@@ -253,7 +254,7 @@ df = pd.DataFrame({"text": [
|
|
|
253
254
|
async def process_data():
|
|
254
255
|
return await df["text"].aio.responses(
|
|
255
256
|
"Analyze sentiment and classify as positive/negative/neutral",
|
|
256
|
-
reasoning={"effort": "none"}, #
|
|
257
|
+
reasoning={"effort": "none"}, # Recommended for reasoning models
|
|
257
258
|
max_concurrency=12 # Allow up to 12 concurrent requests
|
|
258
259
|
)
|
|
259
260
|
|
|
@@ -34,9 +34,10 @@ sentiment = reviews.ai.responses(
|
|
|
34
34
|
reasoning={"effort": "none"}, # Mirrors OpenAI SDK for reasoning models
|
|
35
35
|
)
|
|
36
36
|
print(sentiment.tolist())
|
|
37
|
+
# Output: ['Positive sentiment', 'Negative sentiment']
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
**
|
|
40
|
+
**Pandas tutorial (GitHub Pages):** https://microsoft.github.io/openaivec/examples/pandas/
|
|
40
41
|
|
|
41
42
|
## Benchmarks
|
|
42
43
|
|
|
@@ -55,6 +56,7 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
|
|
|
55
56
|
## Contents
|
|
56
57
|
|
|
57
58
|
- [Why openaivec?](#why-openaivec)
|
|
59
|
+
- [Overview](#overview)
|
|
58
60
|
- [Core Workflows](#core-workflows)
|
|
59
61
|
- [Using with Apache Spark UDFs](#using-with-apache-spark-udfs)
|
|
60
62
|
- [Building Prompts](#building-prompts)
|
|
@@ -66,14 +68,13 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
|
|
|
66
68
|
## Why openaivec?
|
|
67
69
|
|
|
68
70
|
- Drop-in `.ai` and `.aio` accessors keep pandas analysts in familiar tooling.
|
|
69
|
-
- OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, and
|
|
70
|
-
- Smart batching (`BatchingMapProxy`/`AsyncBatchingMapProxy`) dedupes prompts, preserves order, and releases waiters on failure.
|
|
71
|
+
- OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, preserve order, and release waiters on failure.
|
|
71
72
|
- Reasoning support mirrors the OpenAI SDK; structured outputs accept Pydantic `response_format`.
|
|
72
73
|
- Built-in caches and retries remove boilerplate; helpers reuse caches across pandas, Spark, and async flows.
|
|
73
74
|
- Spark UDFs and Microsoft Fabric guides move notebooks into production-scale ETL.
|
|
74
75
|
- Prompt tooling (`FewShotPromptBuilder`, `improve`) and the task library ship curated prompts with validated outputs.
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
## Overview
|
|
77
78
|
|
|
78
79
|
Vectorized OpenAI batch processing so you handle many inputs per call instead of one-by-one. Batching proxies dedupe inputs, enforce ordered outputs, and unblock waiters even on upstream errors. Cache helpers (`responses_with_cache`, Spark UDF builders) plug into the same layer so expensive prompts are reused across pandas, Spark, and async flows. Reasoning models honor SDK semantics. Requires Python 3.10+.
|
|
79
80
|
|
|
@@ -159,7 +160,7 @@ result = df.assign(
|
|
|
159
160
|
|
|
160
161
|
### Using with reasoning models
|
|
161
162
|
|
|
162
|
-
Reasoning models (o1-preview, o1-mini, o3-mini, etc.)
|
|
163
|
+
Reasoning models (o1-preview, o1-mini, o3-mini, etc.) follow OpenAI SDK semantics. Pass `reasoning` when you want to override model defaults.
|
|
163
164
|
|
|
164
165
|
```python
|
|
165
166
|
pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
|
|
@@ -167,7 +168,7 @@ pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
|
|
|
167
168
|
result = df.assign(
|
|
168
169
|
analysis=lambda df: df.text.ai.responses(
|
|
169
170
|
"Analyze this text step by step",
|
|
170
|
-
reasoning={"effort": "none"} # Optional: mirrors the OpenAI SDK argument
|
|
171
|
+
reasoning={"effort": "none"}, # Optional: mirrors the OpenAI SDK argument
|
|
171
172
|
)
|
|
172
173
|
)
|
|
173
174
|
```
|
|
@@ -227,7 +228,7 @@ df = pd.DataFrame({"text": [
|
|
|
227
228
|
async def process_data():
|
|
228
229
|
return await df["text"].aio.responses(
|
|
229
230
|
"Analyze sentiment and classify as positive/negative/neutral",
|
|
230
|
-
reasoning={"effort": "none"}, #
|
|
231
|
+
reasoning={"effort": "none"}, # Recommended for reasoning models
|
|
231
232
|
max_concurrency=12 # Allow up to 12 concurrent requests
|
|
232
233
|
)
|
|
233
234
|
|
|
@@ -21,7 +21,10 @@ class BatchSizeSuggester:
|
|
|
21
21
|
min_batch_size: int = 10
|
|
22
22
|
min_duration: float = 30.0
|
|
23
23
|
max_duration: float = 60.0
|
|
24
|
-
|
|
24
|
+
step_ratio_up: float = 0.1
|
|
25
|
+
step_ratio_down: float = 0.2
|
|
26
|
+
max_step: int | None = None
|
|
27
|
+
min_step: int = 1
|
|
25
28
|
sample_size: int = 4
|
|
26
29
|
_history: list[PerformanceMetric] = field(default_factory=list)
|
|
27
30
|
_lock: threading.RLock = field(default_factory=threading.RLock, repr=False)
|
|
@@ -34,8 +37,14 @@ class BatchSizeSuggester:
|
|
|
34
37
|
raise ValueError("current_batch_size must be >= min_batch_size")
|
|
35
38
|
if self.sample_size <= 0:
|
|
36
39
|
raise ValueError("sample_size must be > 0")
|
|
37
|
-
if self.
|
|
38
|
-
raise ValueError("
|
|
40
|
+
if self.step_ratio_up <= 0:
|
|
41
|
+
raise ValueError("step_ratio_up must be > 0")
|
|
42
|
+
if self.step_ratio_down <= 0:
|
|
43
|
+
raise ValueError("step_ratio_down must be > 0")
|
|
44
|
+
if self.max_step is not None and self.max_step <= 0:
|
|
45
|
+
raise ValueError("max_step must be > 0")
|
|
46
|
+
if self.min_step <= 0:
|
|
47
|
+
raise ValueError("min_step must be > 0")
|
|
39
48
|
if self.min_duration <= 0 or self.max_duration <= 0:
|
|
40
49
|
raise ValueError("min_duration and max_duration must be > 0")
|
|
41
50
|
if self.min_duration >= self.max_duration:
|
|
@@ -94,9 +103,15 @@ class BatchSizeSuggester:
|
|
|
94
103
|
current_size = self.current_batch_size
|
|
95
104
|
|
|
96
105
|
if average_duration < self.min_duration:
|
|
97
|
-
|
|
106
|
+
delta = max(self.min_step, int(current_size * self.step_ratio_up))
|
|
107
|
+
if self.max_step is not None:
|
|
108
|
+
delta = min(delta, self.max_step)
|
|
109
|
+
new_batch_size = current_size + delta
|
|
98
110
|
elif average_duration > self.max_duration:
|
|
99
|
-
|
|
111
|
+
delta = max(self.min_step, int(current_size * self.step_ratio_down))
|
|
112
|
+
if self.max_step is not None:
|
|
113
|
+
delta = min(delta, self.max_step)
|
|
114
|
+
new_batch_size = current_size - delta
|
|
100
115
|
else:
|
|
101
116
|
new_batch_size = current_size
|
|
102
117
|
|
|
@@ -21,6 +21,51 @@ __all__ = []
|
|
|
21
21
|
CONTAINER = di.Container()
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def _build_missing_credentials_error(
|
|
25
|
+
openai_api_key: str | None,
|
|
26
|
+
azure_api_key: str | None,
|
|
27
|
+
azure_base_url: str | None,
|
|
28
|
+
azure_api_version: str | None,
|
|
29
|
+
) -> str:
|
|
30
|
+
"""Build a detailed error message for missing credentials.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
openai_api_key (str | None): The OpenAI API key value.
|
|
34
|
+
azure_api_key (str | None): The Azure OpenAI API key value.
|
|
35
|
+
azure_base_url (str | None): The Azure OpenAI base URL value.
|
|
36
|
+
azure_api_version (str | None): The Azure OpenAI API version value.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
str: A detailed error message with missing variables and setup instructions.
|
|
40
|
+
"""
|
|
41
|
+
lines = ["No valid OpenAI or Azure OpenAI credentials found.", ""]
|
|
42
|
+
|
|
43
|
+
# Check OpenAI
|
|
44
|
+
lines.append("Option 1: Set OPENAI_API_KEY for OpenAI")
|
|
45
|
+
if openai_api_key:
|
|
46
|
+
lines.append(" ✓ OPENAI_API_KEY is set")
|
|
47
|
+
else:
|
|
48
|
+
lines.append(" ✗ OPENAI_API_KEY is not set")
|
|
49
|
+
lines.append(' Example: export OPENAI_API_KEY="sk-..."')
|
|
50
|
+
lines.append("")
|
|
51
|
+
|
|
52
|
+
# Check Azure OpenAI
|
|
53
|
+
lines.append("Option 2: Set all Azure OpenAI variables")
|
|
54
|
+
azure_vars = [
|
|
55
|
+
("AZURE_OPENAI_API_KEY", azure_api_key, '"your-azure-api-key"'),
|
|
56
|
+
("AZURE_OPENAI_BASE_URL", azure_base_url, '"https://YOUR-RESOURCE-NAME.services.ai.azure.com/openai/v1/"'),
|
|
57
|
+
("AZURE_OPENAI_API_VERSION", azure_api_version, '"2024-12-01-preview"'),
|
|
58
|
+
]
|
|
59
|
+
for var_name, var_value, example in azure_vars:
|
|
60
|
+
if var_value:
|
|
61
|
+
lines.append(f" ✓ {var_name} is set")
|
|
62
|
+
else:
|
|
63
|
+
lines.append(f" ✗ {var_name} is not set")
|
|
64
|
+
lines.append(f" Example: export {var_name}={example}")
|
|
65
|
+
|
|
66
|
+
return "\n".join(lines)
|
|
67
|
+
|
|
68
|
+
|
|
24
69
|
def _check_azure_v1_api_url(base_url: str) -> None:
|
|
25
70
|
"""Check if Azure OpenAI base URL uses the recommended v1 API format.
|
|
26
71
|
|
|
@@ -81,9 +126,12 @@ def provide_openai_client() -> OpenAI:
|
|
|
81
126
|
)
|
|
82
127
|
|
|
83
128
|
raise ValueError(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
129
|
+
_build_missing_credentials_error(
|
|
130
|
+
openai_api_key=openai_api_key.value,
|
|
131
|
+
azure_api_key=azure_api_key.value,
|
|
132
|
+
azure_base_url=azure_base_url.value,
|
|
133
|
+
azure_api_version=azure_api_version.value,
|
|
134
|
+
)
|
|
87
135
|
)
|
|
88
136
|
|
|
89
137
|
|
|
@@ -124,9 +172,12 @@ def provide_async_openai_client() -> AsyncOpenAI:
|
|
|
124
172
|
)
|
|
125
173
|
|
|
126
174
|
raise ValueError(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
175
|
+
_build_missing_credentials_error(
|
|
176
|
+
openai_api_key=openai_api_key.value,
|
|
177
|
+
azure_api_key=azure_api_key.value,
|
|
178
|
+
azure_base_url=azure_base_url.value,
|
|
179
|
+
azure_api_version=azure_api_version.value,
|
|
180
|
+
)
|
|
130
181
|
)
|
|
131
182
|
|
|
132
183
|
|
|
@@ -33,21 +33,35 @@ class TestBatchSizeSuggester:
|
|
|
33
33
|
assert suggester.min_batch_size == 10
|
|
34
34
|
assert suggester.min_duration == 30.0
|
|
35
35
|
assert suggester.max_duration == 60.0
|
|
36
|
-
assert suggester.
|
|
36
|
+
assert suggester.step_ratio_up == 0.1
|
|
37
|
+
assert suggester.step_ratio_down == 0.2
|
|
38
|
+
assert suggester.max_step is None
|
|
39
|
+
assert suggester.min_step == 1
|
|
37
40
|
assert suggester.sample_size == 4
|
|
38
41
|
assert len(suggester._history) == 0
|
|
39
42
|
assert suggester._batch_size_changed_at is None
|
|
40
43
|
|
|
41
44
|
def test_custom_initialization(self):
|
|
42
45
|
suggester = BatchSizeSuggester(
|
|
43
|
-
current_batch_size=20,
|
|
46
|
+
current_batch_size=20,
|
|
47
|
+
min_batch_size=5,
|
|
48
|
+
min_duration=15.0,
|
|
49
|
+
max_duration=45.0,
|
|
50
|
+
step_ratio_up=0.15,
|
|
51
|
+
step_ratio_down=0.25,
|
|
52
|
+
min_step=2,
|
|
53
|
+
max_step=5,
|
|
54
|
+
sample_size=5,
|
|
44
55
|
)
|
|
45
56
|
|
|
46
57
|
assert suggester.current_batch_size == 20
|
|
47
58
|
assert suggester.min_batch_size == 5
|
|
48
59
|
assert suggester.min_duration == 15.0
|
|
49
60
|
assert suggester.max_duration == 45.0
|
|
50
|
-
assert suggester.
|
|
61
|
+
assert suggester.step_ratio_up == 0.15
|
|
62
|
+
assert suggester.step_ratio_down == 0.25
|
|
63
|
+
assert suggester.min_step == 2
|
|
64
|
+
assert suggester.max_step == 5
|
|
51
65
|
assert suggester.sample_size == 5
|
|
52
66
|
|
|
53
67
|
@pytest.mark.parametrize(
|
|
@@ -56,7 +70,10 @@ class TestBatchSizeSuggester:
|
|
|
56
70
|
({"min_batch_size": 0}, "min_batch_size must be > 0"),
|
|
57
71
|
({"current_batch_size": 5, "min_batch_size": 10}, "current_batch_size must be >= min_batch_size"),
|
|
58
72
|
({"sample_size": 0}, "sample_size must be > 0"),
|
|
59
|
-
({"
|
|
73
|
+
({"step_ratio_up": 0}, "step_ratio_up must be > 0"),
|
|
74
|
+
({"step_ratio_down": 0}, "step_ratio_down must be > 0"),
|
|
75
|
+
({"min_step": 0}, "min_step must be > 0"),
|
|
76
|
+
({"max_step": 0}, "max_step must be > 0"),
|
|
60
77
|
({"min_duration": 0}, "min_duration and max_duration must be > 0"),
|
|
61
78
|
({"max_duration": 0}, "min_duration and max_duration must be > 0"),
|
|
62
79
|
({"min_duration": 60, "max_duration": 30}, "min_duration must be < max_duration"),
|
|
@@ -190,7 +207,8 @@ class TestBatchSizeSuggester:
|
|
|
190
207
|
min_batch_size=min_batch_size,
|
|
191
208
|
min_duration=0.5, # 0.5 seconds (test scale)
|
|
192
209
|
max_duration=1.0, # 1.0 seconds (test scale)
|
|
193
|
-
|
|
210
|
+
step_ratio_up=0.1,
|
|
211
|
+
step_ratio_down=0.1,
|
|
194
212
|
sample_size=3,
|
|
195
213
|
)
|
|
196
214
|
|
|
@@ -214,7 +232,7 @@ class TestBatchSizeSuggester:
|
|
|
214
232
|
min_batch_size=5,
|
|
215
233
|
min_duration=0.5, # 0.5 seconds (test scale)
|
|
216
234
|
max_duration=1.0, # 1.0 seconds (test scale)
|
|
217
|
-
|
|
235
|
+
step_ratio_down=0.5, # Large step to force below minimum
|
|
218
236
|
sample_size=3,
|
|
219
237
|
)
|
|
220
238
|
|
|
@@ -227,6 +245,60 @@ class TestBatchSizeSuggester:
|
|
|
227
245
|
assert new_size == 5 # Should not go below min_batch_size
|
|
228
246
|
assert suggester.current_batch_size == 5
|
|
229
247
|
|
|
248
|
+
def test_min_step_enforces_minimum_delta_on_increase(self):
|
|
249
|
+
suggester = BatchSizeSuggester(
|
|
250
|
+
current_batch_size=10,
|
|
251
|
+
min_batch_size=1,
|
|
252
|
+
min_duration=0.5,
|
|
253
|
+
max_duration=1.0,
|
|
254
|
+
step_ratio_up=0.1, # 10 * 0.1 = 1
|
|
255
|
+
min_step=5, # force delta to 5
|
|
256
|
+
sample_size=3,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
for i in range(3):
|
|
260
|
+
with suggester.record(batch_size=10):
|
|
261
|
+
time.sleep(0.1) # fast -> increase
|
|
262
|
+
|
|
263
|
+
new_size = suggester.suggest_batch_size()
|
|
264
|
+
assert new_size == 15
|
|
265
|
+
|
|
266
|
+
def test_min_step_enforces_minimum_delta_on_decrease(self):
|
|
267
|
+
suggester = BatchSizeSuggester(
|
|
268
|
+
current_batch_size=20,
|
|
269
|
+
min_batch_size=1,
|
|
270
|
+
min_duration=0.5,
|
|
271
|
+
max_duration=1.0,
|
|
272
|
+
step_ratio_down=0.1, # 20 * 0.1 = 2
|
|
273
|
+
min_step=5, # force delta to 5
|
|
274
|
+
sample_size=3,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
for i in range(3):
|
|
278
|
+
with suggester.record(batch_size=20):
|
|
279
|
+
time.sleep(1.5) # slow -> decrease
|
|
280
|
+
|
|
281
|
+
new_size = suggester.suggest_batch_size()
|
|
282
|
+
assert new_size == 15
|
|
283
|
+
|
|
284
|
+
def test_max_step_caps_delta(self):
|
|
285
|
+
suggester = BatchSizeSuggester(
|
|
286
|
+
current_batch_size=50,
|
|
287
|
+
min_batch_size=1,
|
|
288
|
+
min_duration=0.5,
|
|
289
|
+
max_duration=1.0,
|
|
290
|
+
step_ratio_up=0.5, # 50 * 0.5 = 25
|
|
291
|
+
max_step=10, # cap delta at 10
|
|
292
|
+
sample_size=3,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
for i in range(3):
|
|
296
|
+
with suggester.record(batch_size=50):
|
|
297
|
+
time.sleep(0.1) # fast -> increase
|
|
298
|
+
|
|
299
|
+
new_size = suggester.suggest_batch_size()
|
|
300
|
+
assert new_size == 60
|
|
301
|
+
|
|
230
302
|
def test_thread_safety(self):
|
|
231
303
|
suggester = BatchSizeSuggester(sample_size=10)
|
|
232
304
|
results = []
|
|
@@ -42,7 +42,8 @@ def test_sync_proxy_suggester_adapts_batch_size():
|
|
|
42
42
|
proxy.suggester.min_duration = 0.001 # 1ms
|
|
43
43
|
proxy.suggester.max_duration = 0.002 # 2ms
|
|
44
44
|
proxy.suggester.sample_size = 2
|
|
45
|
-
proxy.suggester.
|
|
45
|
+
proxy.suggester.step_ratio_up = 0.5
|
|
46
|
+
proxy.suggester.step_ratio_down = 0.5
|
|
46
47
|
|
|
47
48
|
import time
|
|
48
49
|
|
|
@@ -4,7 +4,12 @@ import warnings
|
|
|
4
4
|
import pytest
|
|
5
5
|
from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI
|
|
6
6
|
|
|
7
|
-
from openaivec._provider import
|
|
7
|
+
from openaivec._provider import (
|
|
8
|
+
_build_missing_credentials_error,
|
|
9
|
+
provide_async_openai_client,
|
|
10
|
+
provide_openai_client,
|
|
11
|
+
set_default_registrations,
|
|
12
|
+
)
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
class TestProvideOpenAIClient:
|
|
@@ -80,7 +85,7 @@ class TestProvideOpenAIClient:
|
|
|
80
85
|
with pytest.raises(ValueError) as context:
|
|
81
86
|
provide_openai_client()
|
|
82
87
|
|
|
83
|
-
assert "No valid OpenAI or Azure OpenAI
|
|
88
|
+
assert "No valid OpenAI or Azure OpenAI credentials found" in str(context.value)
|
|
84
89
|
|
|
85
90
|
def test_provide_openai_client_with_azure_keys_default_version(self):
|
|
86
91
|
"""Test creating Azure OpenAI client with default API version when not specified."""
|
|
@@ -98,12 +103,16 @@ class TestProvideOpenAIClient:
|
|
|
98
103
|
with pytest.raises(ValueError) as context:
|
|
99
104
|
provide_openai_client()
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
assert
|
|
106
|
+
error_message = str(context.value)
|
|
107
|
+
# Check that the error message contains helpful information
|
|
108
|
+
assert "No valid OpenAI or Azure OpenAI credentials found" in error_message
|
|
109
|
+
assert "OPENAI_API_KEY" in error_message
|
|
110
|
+
assert "AZURE_OPENAI_API_KEY" in error_message
|
|
111
|
+
assert "AZURE_OPENAI_BASE_URL" in error_message
|
|
112
|
+
assert "AZURE_OPENAI_API_VERSION" in error_message
|
|
113
|
+
# Check that setup examples are provided
|
|
114
|
+
assert "export OPENAI_API_KEY" in error_message
|
|
115
|
+
assert "export AZURE_OPENAI_API_KEY" in error_message
|
|
107
116
|
|
|
108
117
|
def test_provide_openai_client_with_empty_openai_key(self):
|
|
109
118
|
"""Test that empty OPENAI_API_KEY is treated as not set."""
|
|
@@ -199,7 +208,7 @@ class TestProvideAsyncOpenAIClient:
|
|
|
199
208
|
with pytest.raises(ValueError) as context:
|
|
200
209
|
provide_async_openai_client()
|
|
201
210
|
|
|
202
|
-
assert "No valid OpenAI or Azure OpenAI
|
|
211
|
+
assert "No valid OpenAI or Azure OpenAI credentials found" in str(context.value)
|
|
203
212
|
|
|
204
213
|
def test_provide_async_openai_client_with_azure_keys_default_version(self):
|
|
205
214
|
"""Test creating async Azure OpenAI client with default API version when not specified."""
|
|
@@ -217,12 +226,16 @@ class TestProvideAsyncOpenAIClient:
|
|
|
217
226
|
with pytest.raises(ValueError) as context:
|
|
218
227
|
provide_async_openai_client()
|
|
219
228
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
assert
|
|
229
|
+
error_message = str(context.value)
|
|
230
|
+
# Check that the error message contains helpful information
|
|
231
|
+
assert "No valid OpenAI or Azure OpenAI credentials found" in error_message
|
|
232
|
+
assert "OPENAI_API_KEY" in error_message
|
|
233
|
+
assert "AZURE_OPENAI_API_KEY" in error_message
|
|
234
|
+
assert "AZURE_OPENAI_BASE_URL" in error_message
|
|
235
|
+
assert "AZURE_OPENAI_API_VERSION" in error_message
|
|
236
|
+
# Check that setup examples are provided
|
|
237
|
+
assert "export OPENAI_API_KEY" in error_message
|
|
238
|
+
assert "export AZURE_OPENAI_API_KEY" in error_message
|
|
226
239
|
|
|
227
240
|
def test_provide_async_openai_client_with_empty_openai_key(self):
|
|
228
241
|
"""Test that empty OPENAI_API_KEY is treated as not set."""
|
|
@@ -399,3 +412,78 @@ class TestAzureV1ApiWarning:
|
|
|
399
412
|
assert "v1 API is recommended" in str(w[0].message)
|
|
400
413
|
|
|
401
414
|
set_default_registrations()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class TestBuildMissingCredentialsError:
|
|
418
|
+
"""Test the _build_missing_credentials_error helper function."""
|
|
419
|
+
|
|
420
|
+
def test_all_variables_missing(self):
|
|
421
|
+
"""Test error message when all variables are missing."""
|
|
422
|
+
message = _build_missing_credentials_error(
|
|
423
|
+
openai_api_key=None,
|
|
424
|
+
azure_api_key=None,
|
|
425
|
+
azure_base_url=None,
|
|
426
|
+
azure_api_version=None,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
assert "No valid OpenAI or Azure OpenAI credentials found" in message
|
|
430
|
+
assert "✗ OPENAI_API_KEY is not set" in message
|
|
431
|
+
assert "✗ AZURE_OPENAI_API_KEY is not set" in message
|
|
432
|
+
assert "✗ AZURE_OPENAI_BASE_URL is not set" in message
|
|
433
|
+
assert "✗ AZURE_OPENAI_API_VERSION is not set" in message
|
|
434
|
+
assert 'export OPENAI_API_KEY="sk-..."' in message
|
|
435
|
+
|
|
436
|
+
def test_only_openai_key_set(self):
|
|
437
|
+
"""Test error message when only OpenAI key is set (but this shouldn't trigger error)."""
|
|
438
|
+
message = _build_missing_credentials_error(
|
|
439
|
+
openai_api_key="sk-test",
|
|
440
|
+
azure_api_key=None,
|
|
441
|
+
azure_base_url=None,
|
|
442
|
+
azure_api_version=None,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
assert "✓ OPENAI_API_KEY is set" in message
|
|
446
|
+
assert "✗ AZURE_OPENAI_API_KEY is not set" in message
|
|
447
|
+
|
|
448
|
+
def test_partial_azure_config(self):
|
|
449
|
+
"""Test error message when Azure config is partially set."""
|
|
450
|
+
message = _build_missing_credentials_error(
|
|
451
|
+
openai_api_key=None,
|
|
452
|
+
azure_api_key="test-key",
|
|
453
|
+
azure_base_url=None,
|
|
454
|
+
azure_api_version="preview",
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
assert "✗ OPENAI_API_KEY is not set" in message
|
|
458
|
+
assert "✓ AZURE_OPENAI_API_KEY is set" in message
|
|
459
|
+
assert "✗ AZURE_OPENAI_BASE_URL is not set" in message
|
|
460
|
+
assert "✓ AZURE_OPENAI_API_VERSION is set" in message
|
|
461
|
+
# Should include example for missing URL
|
|
462
|
+
assert "export AZURE_OPENAI_BASE_URL=" in message
|
|
463
|
+
|
|
464
|
+
def test_all_azure_variables_set(self):
|
|
465
|
+
"""Test error message when all Azure variables are set."""
|
|
466
|
+
message = _build_missing_credentials_error(
|
|
467
|
+
openai_api_key=None,
|
|
468
|
+
azure_api_key="test-key",
|
|
469
|
+
azure_base_url="https://test.openai.azure.com/openai/v1/",
|
|
470
|
+
azure_api_version="preview",
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
assert "✗ OPENAI_API_KEY is not set" in message
|
|
474
|
+
assert "✓ AZURE_OPENAI_API_KEY is set" in message
|
|
475
|
+
assert "✓ AZURE_OPENAI_BASE_URL is set" in message
|
|
476
|
+
assert "✓ AZURE_OPENAI_API_VERSION is set" in message
|
|
477
|
+
|
|
478
|
+
def test_error_message_includes_examples(self):
|
|
479
|
+
"""Test that error message includes setup examples."""
|
|
480
|
+
message = _build_missing_credentials_error(
|
|
481
|
+
openai_api_key=None,
|
|
482
|
+
azure_api_key=None,
|
|
483
|
+
azure_base_url=None,
|
|
484
|
+
azure_api_version=None,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
assert "Option 1: Set OPENAI_API_KEY for OpenAI" in message
|
|
488
|
+
assert "Option 2: Set all Azure OpenAI variables" in message
|
|
489
|
+
assert "Example:" in message
|