batchwork-ai 0.2.2__tar.gz → 0.3.0__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 (64) hide show
  1. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/PKG-INFO +34 -13
  2. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/README.md +33 -12
  3. batchwork_ai-0.3.0/pyproject.toml +98 -0
  4. batchwork_ai-0.2.2/pyproject.toml → batchwork_ai-0.3.0/pyproject.toml.orig +1 -1
  5. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/__init__.py +30 -0
  6. batchwork_ai-0.3.0/src/batchwork/_base_url.py +56 -0
  7. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_limits.py +1 -0
  8. batchwork_ai-0.3.0/src/batchwork/_read_retry.py +89 -0
  9. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/body.py +430 -0
  10. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_commands.py +589 -3
  11. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_config.py +8 -30
  12. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_contract.py +60 -3
  13. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_failures.py +11 -1
  14. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_human.py +20 -0
  15. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_input.py +438 -6
  16. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_lifecycle.py +6 -77
  17. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_registry.py +34 -8
  18. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_submit_text.py +614 -15
  19. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_volume.py +3 -0
  20. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/client.py +388 -67
  21. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/errors.py +1 -1
  22. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/job.py +18 -6
  23. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/media.py +9 -1
  24. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/__init__.py +3 -0
  25. batchwork_ai-0.3.0/src/batchwork/providers/_capabilities.py +34 -0
  26. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/anthropic.py +3 -1
  27. batchwork_ai-0.3.0/src/batchwork/providers/azure.py +76 -0
  28. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/google.py +2 -1
  29. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/mistral.py +32 -18
  30. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/openai_compatible.py +58 -24
  31. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/shared.py +101 -2
  32. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/xai.py +107 -9
  33. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/types.py +174 -2
  34. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/LICENSE +0 -0
  35. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_anthropic_serialization.py +0 -0
  36. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_bounded_json.py +0 -0
  37. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_compatible_media.py +0 -0
  38. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_google_schema.py +0 -0
  39. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_google_serialization.py +0 -0
  40. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_network.py +0 -0
  41. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_provider_failure.py +0 -0
  42. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_serialization.py +0 -0
  43. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_text_validation.py +0 -0
  44. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_typing.py +0 -0
  45. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/__init__.py +0 -0
  46. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_materialize.py +0 -0
  47. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_output.py +0 -0
  48. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_state.py +0 -0
  49. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/_image.py +0 -0
  50. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/adapter.py +0 -0
  51. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/groq.py +0 -0
  52. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/ids.py +0 -0
  53. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/openai.py +0 -0
  54. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/together.py +0 -0
  55. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/py.typed +0 -0
  56. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/__init__.py +0 -0
  57. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/models.py +0 -0
  58. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/poller.py +0 -0
  59. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/signing.py +0 -0
  60. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/transport.py +0 -0
  61. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/stores/__init__.py +0 -0
  62. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/stores/base.py +0 -0
  63. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/stores/memory.py +0 -0
  64. {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/stores/redis.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batchwork-ai
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Unified async batch API for AI providers
5
5
  Keywords: ai,anthropic,batch,gemini,llm,openai
6
6
  Author: Ajanraj
@@ -34,10 +34,10 @@ Description-Content-Type: text/markdown
34
34
 
35
35
  ![Batchwork — one typed API for provider-native AI batch jobs](docs/public/og-image.png)
36
36
 
37
- Unified async batch API for OpenAI, Anthropic, Google Gemini, Groq, Mistral, Together AI, and xAI. Providers price eligible batch requests up to 50% below standard synchronous calls, and Batchwork gives you that pricing through one typed Python interface and one CLI. It handles provider-specific serialization, submission, polling, and result parsing so you do not have to. Pricing and eligibility remain provider- and model-specific.
37
+ Unified async batch API for OpenAI, Azure OpenAI, Anthropic, Google Gemini, Groq, Mistral, Together AI, and xAI. Providers price eligible batch requests up to 50% below standard synchronous calls, and Batchwork gives you that pricing through one typed Python interface and one CLI. It handles provider-specific serialization, submission, polling, and result parsing so you do not have to. Pricing and eligibility remain provider- and model-specific.
38
38
 
39
39
  - One typed API for provider-native batch jobs
40
- - Text, embedding, and image workloads
40
+ - Text, embedding, image, image-edit, moderation, transcription, translation, and video workloads
41
41
  - Normalized jobs, results, usage, and errors
42
42
  - Messages, tools, structured content, and remote media
43
43
  - Polling, persistent stores, and signed webhooks
@@ -129,21 +129,38 @@ asyncio.run(main())
129
129
 
130
130
  Submitting returns a `BatchJob` immediately. Provider processing is asynchronous and may take minutes or, depending on the provider and workload, up to 24 hours.
131
131
 
132
+ Beyond text, the same client covers seven more modalities — for example hosted-audio transcription:
133
+
134
+ ```python
135
+ from batchwork import BatchTranscriptionRequest
136
+
137
+ job = await client.batch_transcriptions(
138
+ model="groq/whisper-large-v3",
139
+ requests=[
140
+ BatchTranscriptionRequest(
141
+ custom_id="call-1",
142
+ audio_url="https://example.com/call-1.mp3",
143
+ )
144
+ ],
145
+ )
146
+ ```
147
+
132
148
  Models use `provider/model` form. Results are correlated with requests through `custom_id`; provider output order is not guaranteed.
133
149
 
134
150
  ## Providers
135
151
 
136
152
  Output workloads supported by Batchwork:
137
153
 
138
- | Provider | Text | Embeddings | Image generation |
139
- | ------------- | ---- | ---------- | ---------------- |
140
- | OpenAI | Yes | Yes | Yes |
141
- | Anthropic | Yes | No | No |
142
- | Google Gemini | Yes | Yes | Yes |
143
- | Groq | Yes | No | No |
144
- | Mistral | Yes | Yes | No |
145
- | Together AI | Yes | No | No |
146
- | xAI | Yes | No | Yes |
154
+ | Provider | Text | Embeddings | Image generation | Image edits | Moderation | Transcription | Translation | Video |
155
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
156
+ | OpenAI | Yes | Yes | Yes | Yes | Yes | No | No | No |
157
+ | Azure OpenAI | Yes | No | No | No | No | No | No | No |
158
+ | Anthropic | Yes | No | No | No | No | No | No | No |
159
+ | Google Gemini | Yes | Yes | Yes | No | No | No | No | No |
160
+ | Groq | Yes | No | No | No | No | Yes | Yes | No |
161
+ | Mistral | Yes | Yes | No | No | Yes | Yes | No | No |
162
+ | Together AI | Yes | No | No | No | No | Yes | Yes | No |
163
+ | xAI | Yes | No | Yes | Yes | No | No | No | Yes |
147
164
 
148
165
  Image, PDF, text-file, and audio inputs for text requests vary separately from output modalities. Provider APIs may also impose model-specific limits. See the [provider overview](https://batchwork.ajanraj.com/docs/providers) for input support, submission transport, credentials, and restrictions.
149
166
 
@@ -165,7 +182,7 @@ The base package does not import or require `upstash-redis`.
165
182
  - [Configuration](https://batchwork.ajanraj.com/docs/configuration)
166
183
  - [Jobs](https://batchwork.ajanraj.com/docs/guides/jobs)
167
184
  - [Results](https://batchwork.ajanraj.com/docs/guides/results)
168
- - [Text, embeddings, and images](https://batchwork.ajanraj.com/docs/modalities/text)
185
+ - [Text, embeddings, and other modalities](https://batchwork.ajanraj.com/docs/modalities/text)
169
186
  - [Provider overview](https://batchwork.ajanraj.com/docs/providers)
170
187
  - [Polling and webhooks](https://batchwork.ajanraj.com/docs/guides/server)
171
188
  - [Stores](https://batchwork.ajanraj.com/docs/guides/stores)
@@ -174,6 +191,10 @@ The base package does not import or require `upstash-redis`.
174
191
  - [Public API](https://batchwork.ajanraj.com/docs/api)
175
192
  - [FAQ](https://batchwork.ajanraj.com/docs/faq)
176
193
 
194
+ ## Built with Codex and GPT-5.6
195
+
196
+ Batchwork was built entirely in the OpenAI Codex CLI with GPT-5.6 during [OpenAI Build Week](https://openai.devpost.com/) (July 2026) — the Python library, the `batchwork` CLI with its machine output contract and local job registry, the Agent Skill, the test suite, and the documentation site. Codex planned the design through a decision interview, implemented the seven provider adapters across three modalities, hardened security through repeated review loops, verified everything against live provider APIs, and drove the PyPI and documentation releases. The full collaboration story, plus a self-contained demo project for judges, lives in [`hackathon/`](hackathon/README.md).
197
+
177
198
  ## License
178
199
 
179
200
  [MIT](https://opensource.org/licenses/MIT) © [Ajanraj](https://github.com/ajanraj)
@@ -2,10 +2,10 @@
2
2
 
3
3
  ![Batchwork — one typed API for provider-native AI batch jobs](docs/public/og-image.png)
4
4
 
5
- Unified async batch API for OpenAI, Anthropic, Google Gemini, Groq, Mistral, Together AI, and xAI. Providers price eligible batch requests up to 50% below standard synchronous calls, and Batchwork gives you that pricing through one typed Python interface and one CLI. It handles provider-specific serialization, submission, polling, and result parsing so you do not have to. Pricing and eligibility remain provider- and model-specific.
5
+ Unified async batch API for OpenAI, Azure OpenAI, Anthropic, Google Gemini, Groq, Mistral, Together AI, and xAI. Providers price eligible batch requests up to 50% below standard synchronous calls, and Batchwork gives you that pricing through one typed Python interface and one CLI. It handles provider-specific serialization, submission, polling, and result parsing so you do not have to. Pricing and eligibility remain provider- and model-specific.
6
6
 
7
7
  - One typed API for provider-native batch jobs
8
- - Text, embedding, and image workloads
8
+ - Text, embedding, image, image-edit, moderation, transcription, translation, and video workloads
9
9
  - Normalized jobs, results, usage, and errors
10
10
  - Messages, tools, structured content, and remote media
11
11
  - Polling, persistent stores, and signed webhooks
@@ -97,21 +97,38 @@ asyncio.run(main())
97
97
 
98
98
  Submitting returns a `BatchJob` immediately. Provider processing is asynchronous and may take minutes or, depending on the provider and workload, up to 24 hours.
99
99
 
100
+ Beyond text, the same client covers seven more modalities — for example hosted-audio transcription:
101
+
102
+ ```python
103
+ from batchwork import BatchTranscriptionRequest
104
+
105
+ job = await client.batch_transcriptions(
106
+ model="groq/whisper-large-v3",
107
+ requests=[
108
+ BatchTranscriptionRequest(
109
+ custom_id="call-1",
110
+ audio_url="https://example.com/call-1.mp3",
111
+ )
112
+ ],
113
+ )
114
+ ```
115
+
100
116
  Models use `provider/model` form. Results are correlated with requests through `custom_id`; provider output order is not guaranteed.
101
117
 
102
118
  ## Providers
103
119
 
104
120
  Output workloads supported by Batchwork:
105
121
 
106
- | Provider | Text | Embeddings | Image generation |
107
- | ------------- | ---- | ---------- | ---------------- |
108
- | OpenAI | Yes | Yes | Yes |
109
- | Anthropic | Yes | No | No |
110
- | Google Gemini | Yes | Yes | Yes |
111
- | Groq | Yes | No | No |
112
- | Mistral | Yes | Yes | No |
113
- | Together AI | Yes | No | No |
114
- | xAI | Yes | No | Yes |
122
+ | Provider | Text | Embeddings | Image generation | Image edits | Moderation | Transcription | Translation | Video |
123
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
124
+ | OpenAI | Yes | Yes | Yes | Yes | Yes | No | No | No |
125
+ | Azure OpenAI | Yes | No | No | No | No | No | No | No |
126
+ | Anthropic | Yes | No | No | No | No | No | No | No |
127
+ | Google Gemini | Yes | Yes | Yes | No | No | No | No | No |
128
+ | Groq | Yes | No | No | No | No | Yes | Yes | No |
129
+ | Mistral | Yes | Yes | No | No | Yes | Yes | No | No |
130
+ | Together AI | Yes | No | No | No | No | Yes | Yes | No |
131
+ | xAI | Yes | No | Yes | Yes | No | No | No | Yes |
115
132
 
116
133
  Image, PDF, text-file, and audio inputs for text requests vary separately from output modalities. Provider APIs may also impose model-specific limits. See the [provider overview](https://batchwork.ajanraj.com/docs/providers) for input support, submission transport, credentials, and restrictions.
117
134
 
@@ -133,7 +150,7 @@ The base package does not import or require `upstash-redis`.
133
150
  - [Configuration](https://batchwork.ajanraj.com/docs/configuration)
134
151
  - [Jobs](https://batchwork.ajanraj.com/docs/guides/jobs)
135
152
  - [Results](https://batchwork.ajanraj.com/docs/guides/results)
136
- - [Text, embeddings, and images](https://batchwork.ajanraj.com/docs/modalities/text)
153
+ - [Text, embeddings, and other modalities](https://batchwork.ajanraj.com/docs/modalities/text)
137
154
  - [Provider overview](https://batchwork.ajanraj.com/docs/providers)
138
155
  - [Polling and webhooks](https://batchwork.ajanraj.com/docs/guides/server)
139
156
  - [Stores](https://batchwork.ajanraj.com/docs/guides/stores)
@@ -142,6 +159,10 @@ The base package does not import or require `upstash-redis`.
142
159
  - [Public API](https://batchwork.ajanraj.com/docs/api)
143
160
  - [FAQ](https://batchwork.ajanraj.com/docs/faq)
144
161
 
162
+ ## Built with Codex and GPT-5.6
163
+
164
+ Batchwork was built entirely in the OpenAI Codex CLI with GPT-5.6 during [OpenAI Build Week](https://openai.devpost.com/) (July 2026) — the Python library, the `batchwork` CLI with its machine output contract and local job registry, the Agent Skill, the test suite, and the documentation site. Codex planned the design through a decision interview, implemented the seven provider adapters across three modalities, hardened security through repeated review loops, verified everything against live provider APIs, and drove the PyPI and documentation releases. The full collaboration story, plus a self-contained demo project for judges, lives in [`hackathon/`](hackathon/README.md).
165
+
145
166
  ## License
146
167
 
147
168
  [MIT](https://opensource.org/licenses/MIT) © [Ajanraj](https://github.com/ajanraj)
@@ -0,0 +1,98 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.29,<0.12"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "batchwork-ai"
7
+ version = "0.3.0"
8
+ description = "Unified async batch API for AI providers"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ keywords = [
14
+ "ai",
15
+ "anthropic",
16
+ "batch",
17
+ "gemini",
18
+ "llm",
19
+ "openai",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 3 - Alpha",
23
+ "Framework :: AsyncIO",
24
+ "Intended Audience :: Developers",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = [
33
+ "click>=8.4,<9",
34
+ "httpcore>=1,<2",
35
+ "httpx>=0.28,<1",
36
+ "pydantic>=2.11,<3",
37
+ ]
38
+
39
+ [[project.authors]]
40
+ name = "Ajanraj"
41
+ email = "hey@ajanraj.com"
42
+
43
+ [[project.maintainers]]
44
+ name = "Ajanraj"
45
+ email = "hey@ajanraj.com"
46
+
47
+ [project.scripts]
48
+ batchwork = "batchwork.cli:main"
49
+
50
+ [project.optional-dependencies]
51
+ redis = ["upstash-redis>=1.4,<2"]
52
+
53
+ [project.urls]
54
+ Documentation = "https://batchwork.ajanraj.com"
55
+ Issues = "https://github.com/ajanraj/batchwork-ai/issues"
56
+ Repository = "https://github.com/ajanraj/batchwork-ai"
57
+
58
+ [dependency-groups]
59
+ dev = [
60
+ "jsonschema>=4.25,<5",
61
+ "pytest>=8.4,<9",
62
+ "pytest-asyncio>=1,<2",
63
+ "pytest-cov>=6.2,<7",
64
+ "ruff>=0.12,<1",
65
+ "twine>=6.1,<7",
66
+ "ty==0.0.59",
67
+ ]
68
+
69
+ [tool.uv.build-backend]
70
+ module-name = "batchwork"
71
+ module-root = "src"
72
+
73
+ [tool.pytest.ini_options]
74
+ addopts = "-q"
75
+ asyncio_mode = "auto"
76
+ testpaths = ["tests"]
77
+ markers = [
78
+ "live: requires real provider credentials",
79
+ "redis: requires Upstash Redis credentials",
80
+ ]
81
+
82
+ [tool.ruff]
83
+ line-length = 100
84
+ target-version = "py311"
85
+
86
+ [tool.ruff.lint]
87
+ select = [
88
+ "E",
89
+ "F",
90
+ "I",
91
+ "UP",
92
+ "B",
93
+ "ASYNC",
94
+ "RUF",
95
+ ]
96
+
97
+ [tool.ty.src]
98
+ include = ["src"]
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "batchwork-ai"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  description = "Unified async batch API for AI providers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -57,8 +57,15 @@ from .types import (
57
57
  BatchEmbeddingRequestSettings,
58
58
  BatchImage,
59
59
  BatchImageDefaults,
60
+ BatchImageEditDefaults,
61
+ BatchImageEditRequest,
62
+ BatchImageFileRef,
63
+ BatchImageRef,
60
64
  BatchImageRequest,
65
+ BatchImageUrlRef,
61
66
  BatchLimits,
67
+ BatchModeration,
68
+ BatchModerationRequest,
62
69
  BatchProvider,
63
70
  BatchRef,
64
71
  BatchRequest,
@@ -69,7 +76,15 @@ from .types import (
69
76
  BatchResultStatus,
70
77
  BatchSnapshot,
71
78
  BatchStatus,
79
+ BatchTranscriptionDefaults,
80
+ BatchTranscriptionRequest,
81
+ BatchTranscriptionSegment,
82
+ BatchTranslationDefaults,
83
+ BatchTranslationRequest,
72
84
  BatchUsage,
85
+ BatchVideo,
86
+ BatchVideoDefaults,
87
+ BatchVideoRequest,
73
88
  ContentToolOutput,
74
89
  CustomPart,
75
90
  DeprecatedToolOutputFileDataPart,
@@ -147,9 +162,16 @@ __all__ = [
147
162
  "BatchEmbeddingRequestSettings",
148
163
  "BatchImage",
149
164
  "BatchImageDefaults",
165
+ "BatchImageEditDefaults",
166
+ "BatchImageEditRequest",
167
+ "BatchImageFileRef",
168
+ "BatchImageRef",
150
169
  "BatchImageRequest",
170
+ "BatchImageUrlRef",
151
171
  "BatchJob",
152
172
  "BatchLimits",
173
+ "BatchModeration",
174
+ "BatchModerationRequest",
153
175
  "BatchPoller",
154
176
  "BatchProvider",
155
177
  "BatchRef",
@@ -164,7 +186,15 @@ __all__ = [
164
186
  "BatchStatus",
165
187
  "BatchStore",
166
188
  "BatchTimeoutError",
189
+ "BatchTranscriptionDefaults",
190
+ "BatchTranscriptionRequest",
191
+ "BatchTranscriptionSegment",
192
+ "BatchTranslationDefaults",
193
+ "BatchTranslationRequest",
167
194
  "BatchUsage",
195
+ "BatchVideo",
196
+ "BatchVideoDefaults",
197
+ "BatchVideoRequest",
168
198
  "BatchWebhookEvent",
169
199
  "Batchwork",
170
200
  "BatchworkError",
@@ -0,0 +1,56 @@
1
+ """Provider base URL validation and normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ipaddress import ip_address
6
+ from urllib.parse import urlsplit, urlunsplit
7
+
8
+
9
+ class BaseUrlError(ValueError):
10
+ """A provider base URL violates the outbound credential policy."""
11
+
12
+
13
+ def _policy_error(label: str) -> BaseUrlError:
14
+ return BaseUrlError(
15
+ f"{label} must be absolute HTTPS (HTTP allowed for loopback), without "
16
+ "userinfo, query, or fragment."
17
+ )
18
+
19
+
20
+ def normalize_base_url(value: str, label: str = "base URL") -> str:
21
+ """Validate a provider endpoint and remove trailing path slashes."""
22
+ raw_scheme, separator, remainder = value.partition("://")
23
+ authority = remainder
24
+ for delimiter in "/?#":
25
+ authority = authority.partition(delimiter)[0]
26
+ try:
27
+ parsed = urlsplit(value)
28
+ hostname = parsed.hostname
29
+ except ValueError as error:
30
+ raise _policy_error(label) from error
31
+ try:
32
+ _ = parsed.port
33
+ except ValueError as error:
34
+ raise BaseUrlError(f"{label} contains an invalid port.") from error
35
+
36
+ loopback = hostname == "localhost"
37
+ if hostname is not None:
38
+ try:
39
+ loopback = loopback or ip_address(hostname).is_loopback
40
+ except ValueError:
41
+ pass
42
+
43
+ if (
44
+ not separator
45
+ or raw_scheme.lower() != parsed.scheme
46
+ or not hostname
47
+ or any(character.isspace() for character in authority)
48
+ or parsed.username is not None
49
+ or parsed.password is not None
50
+ or "?" in value
51
+ or "#" in value
52
+ or (parsed.scheme != "https" and not (parsed.scheme == "http" and loopback))
53
+ ):
54
+ raise _policy_error(label)
55
+
56
+ return urlunsplit((parsed.scheme, parsed.netloc, parsed.path.rstrip("/"), "", ""))
@@ -16,3 +16,4 @@ MAX_AGGREGATE_RESULTS_BYTES = 1024 * 1024 * 1024
16
16
  LARGE_BATCH_REQUESTS = 10_000
17
17
  LARGE_BATCH_UPLOAD_BYTES = 50 * 1024 * 1024
18
18
  LARGE_BATCH_IMAGES = 100
19
+ LARGE_BATCH_VIDEOS = 20
@@ -0,0 +1,89 @@
1
+ """Provider-neutral retry policy for safe read operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import random
7
+ import ssl
8
+ import time
9
+ from collections.abc import Awaitable, Callable
10
+ from typing import TypeVar
11
+
12
+ import httpx
13
+
14
+ from ._provider_failure import ProviderFailureError, ProviderFailureKind
15
+
16
+ _READ_ATTEMPTS = 3
17
+ _RETRYABLE_HTTP_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
18
+ _MAX_RETRY_DELAY_SECONDS = 60.0
19
+ _ReadResult = TypeVar("_ReadResult")
20
+
21
+
22
+ class _ReadRetryDeadlineExceeded(TimeoutError):
23
+ """The local read-retry deadline expired before another attempt."""
24
+
25
+
26
+ def _cause_chain(error: BaseException) -> list[BaseException]:
27
+ causes: list[BaseException] = []
28
+ current: BaseException | None = error
29
+ while current is not None and current not in causes:
30
+ causes.append(current)
31
+ current = current.__cause__ or current.__context__
32
+ return causes
33
+
34
+
35
+ def _retryable_read_failure(error: BaseException) -> bool:
36
+ if not isinstance(error, ProviderFailureError):
37
+ return False
38
+ failure = error.failure
39
+ if failure.kind is ProviderFailureKind.UNAVAILABLE:
40
+ return failure.status_code in _RETRYABLE_HTTP_STATUSES
41
+ if failure.kind is not ProviderFailureKind.TRANSPORT:
42
+ return False
43
+ causes = _cause_chain(error)
44
+ if any(isinstance(cause, ssl.SSLError) for cause in causes):
45
+ return False
46
+ return any(
47
+ isinstance(
48
+ cause,
49
+ (
50
+ httpx.ConnectError,
51
+ httpx.ConnectTimeout,
52
+ httpx.ReadError,
53
+ httpx.ReadTimeout,
54
+ ),
55
+ )
56
+ for cause in causes
57
+ )
58
+
59
+
60
+ def _retry_delay(error: BaseException, retry_index: int) -> float:
61
+ if isinstance(error, ProviderFailureError):
62
+ retry_after = error.failure.retry_after_seconds
63
+ if retry_after is not None:
64
+ return min(float(retry_after), _MAX_RETRY_DELAY_SECONDS)
65
+ ceiling = min(0.25 * (2**retry_index), _MAX_RETRY_DELAY_SECONDS)
66
+ return random.uniform(ceiling / 2, ceiling)
67
+
68
+
69
+ async def _retry_read(
70
+ read: Callable[[], Awaitable[_ReadResult]],
71
+ *,
72
+ deadline: float | None = None,
73
+ ) -> _ReadResult:
74
+ for attempt in range(_READ_ATTEMPTS):
75
+ try:
76
+ return await read()
77
+ except Exception as error:
78
+ if attempt + 1 == _READ_ATTEMPTS or not _retryable_read_failure(error):
79
+ raise
80
+ delay = _retry_delay(error, attempt)
81
+ if deadline is not None:
82
+ remaining = deadline - time.monotonic()
83
+ if remaining <= 0:
84
+ raise _ReadRetryDeadlineExceeded from None
85
+ if delay >= remaining:
86
+ await asyncio.sleep(remaining)
87
+ raise _ReadRetryDeadlineExceeded from None
88
+ await asyncio.sleep(delay)
89
+ raise RuntimeError("batchwork: exhausted retry loop")