openaivec 1.0.10__tar.gz → 1.0.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. openaivec-1.0.12/.github/workflows/publish.yml +59 -0
  2. {openaivec-1.0.10 → openaivec-1.0.12}/.github/workflows/test-pr.yml +14 -0
  3. {openaivec-1.0.10 → openaivec-1.0.12}/PKG-INFO +8 -8
  4. {openaivec-1.0.10 → openaivec-1.0.12}/README.md +7 -7
  5. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_cache/optimize.py +20 -5
  6. {openaivec-1.0.10 → openaivec-1.0.12}/tests/_cache/test_optimize.py +78 -6
  7. {openaivec-1.0.10 → openaivec-1.0.12}/tests/_cache/test_proxy_suggester.py +2 -1
  8. {openaivec-1.0.10 → openaivec-1.0.12}/uv.lock +237 -237
  9. openaivec-1.0.10/.github/workflows/publish.yml +0 -34
  10. {openaivec-1.0.10 → openaivec-1.0.12}/.env.example +0 -0
  11. {openaivec-1.0.10 → openaivec-1.0.12}/.github/copilot-instructions.md +0 -0
  12. {openaivec-1.0.10 → openaivec-1.0.12}/.github/dependabot.yml +0 -0
  13. {openaivec-1.0.10 → openaivec-1.0.12}/.github/workflows/docs.yml +0 -0
  14. {openaivec-1.0.10 → openaivec-1.0.12}/.github/workflows/test.yml +0 -0
  15. {openaivec-1.0.10 → openaivec-1.0.12}/.gitignore +0 -0
  16. {openaivec-1.0.10 → openaivec-1.0.12}/AGENTS.md +0 -0
  17. {openaivec-1.0.10 → openaivec-1.0.12}/CODE_OF_CONDUCT.md +0 -0
  18. {openaivec-1.0.10 → openaivec-1.0.12}/LICENSE +0 -0
  19. {openaivec-1.0.10 → openaivec-1.0.12}/SECURITY.md +0 -0
  20. {openaivec-1.0.10 → openaivec-1.0.12}/SUPPORT.md +0 -0
  21. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/main.md +0 -0
  22. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/pandas_ext.md +0 -0
  23. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/spark.md +0 -0
  24. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/task.md +0 -0
  25. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/customer_sentiment.md +0 -0
  26. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/inquiry_classification.md +0 -0
  27. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/inquiry_summary.md +0 -0
  28. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/intent_analysis.md +0 -0
  29. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/response_suggestion.md +0 -0
  30. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/urgency_analysis.md +0 -0
  31. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/dependency_parsing.md +0 -0
  32. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/keyword_extraction.md +0 -0
  33. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/morphological_analysis.md +0 -0
  34. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/named_entity_recognition.md +0 -0
  35. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/sentiment_analysis.md +0 -0
  36. {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/translation.md +0 -0
  37. {openaivec-1.0.10 → openaivec-1.0.12}/docs/contributor-guide.md +0 -0
  38. {openaivec-1.0.10 → openaivec-1.0.12}/docs/index.md +0 -0
  39. {openaivec-1.0.10 → openaivec-1.0.12}/docs/overrides/main.html +0 -0
  40. {openaivec-1.0.10 → openaivec-1.0.12}/docs/robots.txt +0 -0
  41. {openaivec-1.0.10 → openaivec-1.0.12}/mkdocs.yml +0 -0
  42. {openaivec-1.0.10 → openaivec-1.0.12}/pyproject.toml +0 -0
  43. {openaivec-1.0.10 → openaivec-1.0.12}/pytest.ini +0 -0
  44. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/__init__.py +0 -0
  45. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_cache/__init__.py +0 -0
  46. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_cache/proxy.py +0 -0
  47. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_di.py +0 -0
  48. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_embeddings.py +0 -0
  49. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_log.py +0 -0
  50. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_model.py +0 -0
  51. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_prompt.py +0 -0
  52. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_provider.py +0 -0
  53. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_responses.py +0 -0
  54. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_schema/__init__.py +0 -0
  55. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_schema/infer.py +0 -0
  56. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_schema/spec.py +0 -0
  57. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_serialize.py +0 -0
  58. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_util.py +0 -0
  59. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/pandas_ext.py +0 -0
  60. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/spark.py +0 -0
  61. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/__init__.py +0 -0
  62. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/__init__.py +0 -0
  63. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/customer_sentiment.py +0 -0
  64. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/inquiry_classification.py +0 -0
  65. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/inquiry_summary.py +0 -0
  66. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/intent_analysis.py +0 -0
  67. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/response_suggestion.py +0 -0
  68. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/urgency_analysis.py +0 -0
  69. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/__init__.py +0 -0
  70. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/dependency_parsing.py +0 -0
  71. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/keyword_extraction.py +0 -0
  72. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/morphological_analysis.py +0 -0
  73. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/named_entity_recognition.py +0 -0
  74. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/sentiment_analysis.py +0 -0
  75. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/translation.py +0 -0
  76. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/table/__init__.py +0 -0
  77. {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/table/fillna.py +0 -0
  78. {openaivec-1.0.10 → openaivec-1.0.12}/tests/__init__.py +0 -0
  79. {openaivec-1.0.10 → openaivec-1.0.12}/tests/_cache/test_proxy.py +0 -0
  80. {openaivec-1.0.10 → openaivec-1.0.12}/tests/_schema/test_infer.py +0 -0
  81. {openaivec-1.0.10 → openaivec-1.0.12}/tests/_schema/test_spec.py +0 -0
  82. {openaivec-1.0.10 → openaivec-1.0.12}/tests/conftest.py +0 -0
  83. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_di.py +0 -0
  84. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_embeddings.py +0 -0
  85. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_pandas_ext.py +0 -0
  86. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_prompt.py +0 -0
  87. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_provider.py +0 -0
  88. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_responses.py +0 -0
  89. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_serialize.py +0 -0
  90. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_serialize_pydantic_v2_compliance.py +0 -0
  91. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_spark.py +0 -0
  92. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_task.py +0 -0
  93. {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_util.py +0 -0
@@ -0,0 +1,59 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ permissions:
9
+ contents: write
10
+ id-token: write
11
+
12
+ jobs:
13
+ build-and-publish:
14
+ runs-on: ubuntu-latest
15
+ environment: pypi
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v7
23
+
24
+ - name: Set up Python
25
+ run: uv python install 3.10
26
+
27
+ - name: Install dependencies via uv
28
+ run: uv sync --all-extras --dev
29
+
30
+ - name: Build with uv
31
+ run: uv build
32
+
33
+ - name: Install cosign
34
+ uses: sigstore/cosign-installer@v3
35
+
36
+ - name: Sign artifacts with cosign (OIDC)
37
+ run: |
38
+ set -euo pipefail
39
+ ls -la dist
40
+ mkdir -p signatures
41
+ for f in dist/*; do
42
+ if [ -f "$f" ]; then
43
+ base="$(basename "$f")"
44
+ cosign sign-blob --yes \
45
+ --output-signature "signatures/${base}.sig" \
46
+ --output-certificate "signatures/${base}.pem" \
47
+ "$f"
48
+ fi
49
+ done
50
+
51
+ - name: Create GitHub Release
52
+ uses: softprops/action-gh-release@v2
53
+ with:
54
+ files: |
55
+ dist/*
56
+ signatures/*
57
+
58
+ - name: Publish to PyPI
59
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -6,6 +6,7 @@ on:
6
6
 
7
7
  permissions:
8
8
  contents: read
9
+ issues: write
9
10
  pull-requests: read
10
11
 
11
12
  jobs:
@@ -23,6 +24,19 @@ jobs:
23
24
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
24
25
 
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
+
26
40
  - name: Fetch PR head SHA
27
41
  id: pr
28
42
  shell: bash
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openaivec
3
- Version: 1.0.10
3
+ Version: 1.0.12
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
@@ -63,7 +63,7 @@ print(sentiment.tolist())
63
63
  # Output: ['Positive sentiment', 'Negative sentiment']
64
64
  ```
65
65
 
66
- **Try it live:** https://microsoft.github.io/openaivec/examples/pandas/
66
+ **Pandas tutorial (GitHub Pages):** https://microsoft.github.io/openaivec/examples/pandas/
67
67
 
68
68
  ## Benchmarks
69
69
 
@@ -82,6 +82,7 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
82
82
  ## Contents
83
83
 
84
84
  - [Why openaivec?](#why-openaivec)
85
+ - [Overview](#overview)
85
86
  - [Core Workflows](#core-workflows)
86
87
  - [Using with Apache Spark UDFs](#using-with-apache-spark-udfs)
87
88
  - [Building Prompts](#building-prompts)
@@ -93,14 +94,13 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
93
94
  ## Why openaivec?
94
95
 
95
96
  - Drop-in `.ai` and `.aio` accessors keep pandas analysts in familiar tooling.
96
- - OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, and keep column order stable.
97
- - 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.
98
98
  - Reasoning support mirrors the OpenAI SDK; structured outputs accept Pydantic `response_format`.
99
99
  - Built-in caches and retries remove boilerplate; helpers reuse caches across pandas, Spark, and async flows.
100
100
  - Spark UDFs and Microsoft Fabric guides move notebooks into production-scale ETL.
101
101
  - Prompt tooling (`FewShotPromptBuilder`, `improve`) and the task library ship curated prompts with validated outputs.
102
102
 
103
- # Overview
103
+ ## Overview
104
104
 
105
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+.
106
106
 
@@ -186,7 +186,7 @@ result = df.assign(
186
186
 
187
187
  ### Using with reasoning models
188
188
 
189
- Reasoning models (o1-preview, o1-mini, o3-mini, etc.) work without special flags. `reasoning` mirrors the OpenAI SDK.
189
+ Reasoning models (o1-preview, o1-mini, o3-mini, etc.) follow OpenAI SDK semantics. Pass `reasoning` when you want to override model defaults.
190
190
 
191
191
  ```python
192
192
  pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
@@ -194,7 +194,7 @@ pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
194
194
  result = df.assign(
195
195
  analysis=lambda df: df.text.ai.responses(
196
196
  "Analyze this text step by step",
197
- reasoning={"effort": "none"} # Optional: mirrors the OpenAI SDK argument
197
+ reasoning={"effort": "none"}, # Optional: mirrors the OpenAI SDK argument
198
198
  )
199
199
  )
200
200
  ```
@@ -254,7 +254,7 @@ df = pd.DataFrame({"text": [
254
254
  async def process_data():
255
255
  return await df["text"].aio.responses(
256
256
  "Analyze sentiment and classify as positive/negative/neutral",
257
- reasoning={"effort": "none"}, # Required for gpt-5.1
257
+ reasoning={"effort": "none"}, # Recommended for reasoning models
258
258
  max_concurrency=12 # Allow up to 12 concurrent requests
259
259
  )
260
260
 
@@ -37,7 +37,7 @@ print(sentiment.tolist())
37
37
  # Output: ['Positive sentiment', 'Negative sentiment']
38
38
  ```
39
39
 
40
- **Try it live:** https://microsoft.github.io/openaivec/examples/pandas/
40
+ **Pandas tutorial (GitHub Pages):** https://microsoft.github.io/openaivec/examples/pandas/
41
41
 
42
42
  ## Benchmarks
43
43
 
@@ -56,6 +56,7 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
56
56
  ## Contents
57
57
 
58
58
  - [Why openaivec?](#why-openaivec)
59
+ - [Overview](#overview)
59
60
  - [Core Workflows](#core-workflows)
60
61
  - [Using with Apache Spark UDFs](#using-with-apache-spark-udfs)
61
62
  - [Building Prompts](#building-prompts)
@@ -67,14 +68,13 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
67
68
  ## Why openaivec?
68
69
 
69
70
  - Drop-in `.ai` and `.aio` accessors keep pandas analysts in familiar tooling.
70
- - OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, and keep column order stable.
71
- - 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.
72
72
  - Reasoning support mirrors the OpenAI SDK; structured outputs accept Pydantic `response_format`.
73
73
  - Built-in caches and retries remove boilerplate; helpers reuse caches across pandas, Spark, and async flows.
74
74
  - Spark UDFs and Microsoft Fabric guides move notebooks into production-scale ETL.
75
75
  - Prompt tooling (`FewShotPromptBuilder`, `improve`) and the task library ship curated prompts with validated outputs.
76
76
 
77
- # Overview
77
+ ## Overview
78
78
 
79
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+.
80
80
 
@@ -160,7 +160,7 @@ result = df.assign(
160
160
 
161
161
  ### Using with reasoning models
162
162
 
163
- Reasoning models (o1-preview, o1-mini, o3-mini, etc.) work without special flags. `reasoning` mirrors the OpenAI SDK.
163
+ Reasoning models (o1-preview, o1-mini, o3-mini, etc.) follow OpenAI SDK semantics. Pass `reasoning` when you want to override model defaults.
164
164
 
165
165
  ```python
166
166
  pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
@@ -168,7 +168,7 @@ pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
168
168
  result = df.assign(
169
169
  analysis=lambda df: df.text.ai.responses(
170
170
  "Analyze this text step by step",
171
- reasoning={"effort": "none"} # Optional: mirrors the OpenAI SDK argument
171
+ reasoning={"effort": "none"}, # Optional: mirrors the OpenAI SDK argument
172
172
  )
173
173
  )
174
174
  ```
@@ -228,7 +228,7 @@ df = pd.DataFrame({"text": [
228
228
  async def process_data():
229
229
  return await df["text"].aio.responses(
230
230
  "Analyze sentiment and classify as positive/negative/neutral",
231
- reasoning={"effort": "none"}, # Required for gpt-5.1
231
+ reasoning={"effort": "none"}, # Recommended for reasoning models
232
232
  max_concurrency=12 # Allow up to 12 concurrent requests
233
233
  )
234
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
- step_ratio: float = 0.2
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.step_ratio <= 0:
38
- raise ValueError("step_ratio must be > 0")
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
- new_batch_size = int(current_size * (1 + self.step_ratio))
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
- new_batch_size = int(current_size * (1 - self.step_ratio))
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
 
@@ -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.step_ratio == 0.2
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, min_batch_size=5, min_duration=15.0, max_duration=45.0, step_ratio=0.2, sample_size=5
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.step_ratio == 0.2
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
- ({"step_ratio": 0}, "step_ratio must be > 0"),
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
- step_ratio=0.1,
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
- step_ratio=0.5, # Large step to force below minimum
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.step_ratio = 0.5
45
+ proxy.suggester.step_ratio_up = 0.5
46
+ proxy.suggester.step_ratio_down = 0.5
46
47
 
47
48
  import time
48
49