batchor 0.0.3__tar.gz → 0.0.4__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 (117) hide show
  1. {batchor-0.0.3 → batchor-0.0.4}/.agents/plugins/marketplace.json +12 -0
  2. {batchor-0.0.3 → batchor-0.0.4}/PKG-INFO +102 -14
  3. {batchor-0.0.3 → batchor-0.0.4}/README.md +96 -11
  4. {batchor-0.0.3 → batchor-0.0.4}/docs/design_docs/ARCHITECTURE.md +16 -7
  5. batchor-0.0.4/docs/design_docs/GEMINI_BATCHING.md +96 -0
  6. {batchor-0.0.3 → batchor-0.0.4}/docs/design_docs/OPENAI_BATCHING.md +3 -2
  7. {batchor-0.0.3 → batchor-0.0.4}/docs/design_docs/ROADMAP.md +2 -1
  8. {batchor-0.0.3 → batchor-0.0.4}/docs/design_docs/STORAGE_AND_RUNS.md +3 -2
  9. {batchor-0.0.3 → batchor-0.0.4}/docs/doc-map.md +2 -0
  10. {batchor-0.0.3 → batchor-0.0.4}/docs/getting-started/cli.md +43 -0
  11. {batchor-0.0.3 → batchor-0.0.4}/docs/getting-started/installation.md +37 -1
  12. {batchor-0.0.3 → batchor-0.0.4}/docs/getting-started/python-api.md +41 -0
  13. {batchor-0.0.3 → batchor-0.0.4}/docs/index.md +2 -2
  14. {batchor-0.0.3 → batchor-0.0.4}/docs/reference/api.md +10 -0
  15. {batchor-0.0.3 → batchor-0.0.4}/docs/smoke-test.md +28 -0
  16. {batchor-0.0.3 → batchor-0.0.4}/mkdocs.yml +2 -1
  17. batchor-0.0.4/plugins/batchor/.codex-plugin/plugin.json +44 -0
  18. batchor-0.0.4/plugins/batchor/.mcp.json +10 -0
  19. batchor-0.0.4/plugins/batchor/scripts/batchor_user_mcp.py +205 -0
  20. batchor-0.0.4/plugins/batchor/skills/use-batchor/SKILL.md +57 -0
  21. batchor-0.0.4/plugins/batchor/skills/use-batchor/agents/openai.yaml +4 -0
  22. batchor-0.0.4/plugins/batchor/skills/use-batchor/references/cli-workflows.md +45 -0
  23. batchor-0.0.4/plugins/batchor/skills/use-batchor/references/python-pipelines.md +64 -0
  24. {batchor-0.0.3 → batchor-0.0.4}/plugins/batchor-agent-tools/scripts/batchor_repo_mcp.py +10 -3
  25. {batchor-0.0.3 → batchor-0.0.4}/pyproject.toml +10 -4
  26. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/__init__.py +8 -2
  27. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/cli.py +93 -3
  28. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/core/enums.py +16 -0
  29. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/core/models.py +148 -1
  30. batchor-0.0.4/src/batchor/core/responses.py +60 -0
  31. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/core/types.py +10 -7
  32. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/providers/base.py +33 -3
  33. batchor-0.0.4/src/batchor/providers/gemini.py +554 -0
  34. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/providers/registry.py +20 -7
  35. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/polling.py +3 -2
  36. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/submission.py +13 -9
  37. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/validation.py +12 -67
  38. batchor-0.0.4/tests/integration/test_batchor_gemini_runner.py +219 -0
  39. batchor-0.0.4/tests/integration/test_batchor_live_gemini.py +157 -0
  40. {batchor-0.0.3 → batchor-0.0.4}/tests/integration/test_batchor_runner.py +15 -0
  41. batchor-0.0.4/tests/unit/test_agent_tooling.py +163 -0
  42. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_architecture.py +31 -0
  43. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_cli.py +123 -1
  44. batchor-0.0.4/tests/unit/test_batchor_gemini_provider.py +355 -0
  45. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_runtime_polling.py +3 -0
  46. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_runtime_submission.py +94 -2
  47. {batchor-0.0.3 → batchor-0.0.4}/uv.lock +367 -1
  48. batchor-0.0.3/tests/unit/test_agent_tooling.py +0 -49
  49. {batchor-0.0.3 → batchor-0.0.4}/.agents/skills/batchor-dev/SKILL.md +0 -0
  50. {batchor-0.0.3 → batchor-0.0.4}/.agents/skills/batchor-dev/agents/openai.yaml +0 -0
  51. {batchor-0.0.3 → batchor-0.0.4}/.github/workflows/ci.yml +0 -0
  52. {batchor-0.0.3 → batchor-0.0.4}/.github/workflows/docs.yml +0 -0
  53. {batchor-0.0.3 → batchor-0.0.4}/.github/workflows/publish.yml +0 -0
  54. {batchor-0.0.3 → batchor-0.0.4}/.gitignore +0 -0
  55. {batchor-0.0.3 → batchor-0.0.4}/.pre-commit-config.yaml +0 -0
  56. {batchor-0.0.3 → batchor-0.0.4}/AGENTS.md +0 -0
  57. {batchor-0.0.3 → batchor-0.0.4}/CONTRIBUTING.md +0 -0
  58. {batchor-0.0.3 → batchor-0.0.4}/LICENSE +0 -0
  59. {batchor-0.0.3 → batchor-0.0.4}/SUPPORT.md +0 -0
  60. {batchor-0.0.3 → batchor-0.0.4}/VERSIONING.md +0 -0
  61. {batchor-0.0.3 → batchor-0.0.4}/docs/assets/stylesheets/extra.css +0 -0
  62. {batchor-0.0.3 → batchor-0.0.4}/docs/design_docs/BOUNDARY_AND_PHILOSOPHY.md +0 -0
  63. {batchor-0.0.3 → batchor-0.0.4}/docs/design_docs/STORAGE_MIGRATIONS.md +0 -0
  64. {batchor-0.0.3 → batchor-0.0.4}/docs/getting-started/use-cases.md +0 -0
  65. {batchor-0.0.3 → batchor-0.0.4}/docs/policies/contributing.md +0 -0
  66. {batchor-0.0.3 → batchor-0.0.4}/docs/policies/support.md +0 -0
  67. {batchor-0.0.3 → batchor-0.0.4}/docs/policies/versioning.md +0 -0
  68. {batchor-0.0.3 → batchor-0.0.4}/plugins/batchor-agent-tools/.codex-plugin/plugin.json +0 -0
  69. {batchor-0.0.3 → batchor-0.0.4}/plugins/batchor-agent-tools/.mcp.json +0 -0
  70. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/artifacts/__init__.py +0 -0
  71. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/artifacts/base.py +0 -0
  72. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/artifacts/local.py +0 -0
  73. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/core/__init__.py +0 -0
  74. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/core/exceptions.py +0 -0
  75. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/providers/__init__.py +0 -0
  76. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/providers/openai.py +0 -0
  77. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/py.typed +0 -0
  78. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/__init__.py +0 -0
  79. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/artifacts.py +0 -0
  80. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/context.py +0 -0
  81. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/ingestion.py +0 -0
  82. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/results.py +0 -0
  83. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/retry.py +0 -0
  84. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/run_handle.py +0 -0
  85. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/runner.py +0 -0
  86. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/runtime/tokens.py +0 -0
  87. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/sources/__init__.py +0 -0
  88. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/sources/base.py +0 -0
  89. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/sources/composite.py +0 -0
  90. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/sources/files.py +0 -0
  91. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/__init__.py +0 -0
  92. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/memory.py +0 -0
  93. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/postgres.py +0 -0
  94. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/postgres_store.py +0 -0
  95. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/registry.py +0 -0
  96. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/sqlite.py +0 -0
  97. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/sqlite_codec.py +0 -0
  98. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/sqlite_lifecycle.py +0 -0
  99. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/sqlite_protocol.py +0 -0
  100. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/sqlite_queries.py +0 -0
  101. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/sqlite_results.py +0 -0
  102. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/sqlite_schema.py +0 -0
  103. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/sqlite_store.py +0 -0
  104. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/state.py +0 -0
  105. {batchor-0.0.3 → batchor-0.0.4}/src/batchor/storage/state_models.py +0 -0
  106. {batchor-0.0.3 → batchor-0.0.4}/tests/integration/test_batchor_live_openai.py +0 -0
  107. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_artifacts.py +0 -0
  108. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_openai_provider.py +0 -0
  109. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_results.py +0 -0
  110. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_retry.py +0 -0
  111. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_runtime_ingestion.py +0 -0
  112. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_sources.py +0 -0
  113. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_sqlite_storage_flow.py +0 -0
  114. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_storage.py +0 -0
  115. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_storage_contracts.py +0 -0
  116. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_tokens.py +0 -0
  117. {batchor-0.0.3 → batchor-0.0.4}/tests/unit/test_batchor_validation.py +0 -0
@@ -15,6 +15,18 @@
15
15
  "authentication": "ON_INSTALL"
16
16
  },
17
17
  "category": "Coding"
18
+ },
19
+ {
20
+ "name": "batchor",
21
+ "source": {
22
+ "source": "local",
23
+ "path": "./plugins/batchor"
24
+ },
25
+ "policy": {
26
+ "installation": "AVAILABLE",
27
+ "authentication": "ON_INSTALL"
28
+ },
29
+ "category": "Coding"
18
30
  }
19
31
  ]
20
32
  }
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batchor
3
- Version: 0.0.3
4
- Summary: Structured-first OpenAI Batch runner with typed Pydantic results.
3
+ Version: 0.0.4
4
+ Summary: Structured-first provider Batch runner with typed Pydantic results.
5
5
  Project-URL: Documentation, https://github.com/AnsonDev42/batchor#readme
6
6
  Project-URL: Repository, https://github.com/AnsonDev42/batchor
7
7
  Project-URL: Issues, https://github.com/AnsonDev42/batchor/issues
@@ -79,7 +79,7 @@ License: # PolyForm Noncommercial License 1.0.0
79
79
 
80
80
  **Use** means anything you do with the software requiring one of your licenses.
81
81
  License-File: LICENSE
82
- Keywords: batch,cli,openai,pydantic,sqlite
82
+ Keywords: batch,cli,gemini,openai,pydantic,sqlite
83
83
  Classifier: Development Status :: 3 - Alpha
84
84
  Classifier: Environment :: Console
85
85
  Classifier: Intended Audience :: Developers
@@ -99,13 +99,16 @@ Requires-Dist: python-dotenv<2,>=1.0.1
99
99
  Requires-Dist: sqlalchemy<3,>=2.0.43
100
100
  Requires-Dist: tiktoken>=0.12.0
101
101
  Requires-Dist: typer<1,>=0.16.0
102
+ Provides-Extra: gemini
103
+ Requires-Dist: google-cloud-storage<4,>=3.4.0; extra == 'gemini'
104
+ Requires-Dist: google-genai<2,>=1.55.0; extra == 'gemini'
102
105
  Description-Content-Type: text/markdown
103
106
 
104
107
  # batchor
105
108
 
106
109
  `batchor` grew out of a recurring problem in academic research: running large datasets through LLMs in batch — reliably, reproducibly, and without reinventing the same glue code across every project. The patterns that kept emerging (durable state, typed results, safe resume after failure) were extracted into this library so they do not have to be rebuilt each time.
107
110
 
108
- `batchor` is a durable OpenAI Batch runner for Python teams that want:
111
+ `batchor` is a durable provider Batch runner for Python teams that want:
109
112
 
110
113
  - typed Pydantic results
111
114
  - resumable durable runs
@@ -115,7 +118,7 @@ Description-Content-Type: text/markdown
115
118
  - library-first run controls
116
119
  - a small operator CLI for CSV and JSONL jobs
117
120
 
118
- It is intentionally narrow today: OpenAI-first, SQLite-first, and library-first.
121
+ It is intentionally narrow today: OpenAI is the CLI default, Gemini is opt-in through `batchor[gemini]`, SQLite is the CLI durability backend, and the Python API exposes the broadest configuration surface.
119
122
 
120
123
  ## What problem it solves
121
124
 
@@ -140,6 +143,7 @@ Most OpenAI Batch examples stop at "upload a JSONL file and poll until it finish
140
143
  Built-in implementations:
141
144
 
142
145
  - `OpenAIProviderConfig` + `OpenAIBatchProvider`
146
+ - `GeminiProviderConfig` + `GeminiBatchProvider` for text-only Gemini Batch jobs
143
147
  - `SQLiteStorage`
144
148
  - `PostgresStorage` as an opt-in durable control-plane backend
145
149
  - `MemoryStateStore`
@@ -155,6 +159,8 @@ Important constraints:
155
159
  - the CLI supports file-backed inputs only
156
160
  - users still own selecting and ordering input files or partitions
157
161
  - the built-in CLI uses SQLite durability only
162
+ - the CLI supports OpenAI plus Gemini Developer API and Vertex AI text jobs
163
+ - Gemini support is text-only for now and does not build multimodal requests
158
164
  - structured-output rehydration requires an importable module-level Pydantic model
159
165
  - raw output artifacts are retained by default and must be exported before raw pruning
160
166
  - pause/resume/cancel and incremental terminal-result APIs are library-first today
@@ -193,6 +199,7 @@ graph LR
193
199
 
194
200
  subgraph providers["providers/"]
195
201
  OpenAI["OpenAIBatchProvider"]
202
+ Gemini["GeminiBatchProvider"]
196
203
  end
197
204
 
198
205
  subgraph sources["sources/"]
@@ -212,6 +219,7 @@ graph LR
212
219
  User -->|"start() / run_and_wait()"| BatchRunner
213
220
  BatchRunner --> Run
214
221
  BatchRunner --> OpenAI
222
+ BatchRunner --> Gemini
215
223
  BatchRunner --> SQLite
216
224
  BatchRunner --> LocalFS
217
225
  Files -->|"BatchItem stream"| BatchRunner
@@ -247,16 +255,31 @@ Operational semantics for resume, run control, and artifact retention live in
247
255
  pip install batchor
248
256
  ```
249
257
 
250
- ## Repo Agent Setup
258
+ For Gemini Batch support, install the optional extra:
251
259
 
252
- This repo now includes local AI-agent scaffolding so a contributor agent can pick up repo conventions without extra global setup:
260
+ ```bash
261
+ pip install "batchor[gemini]"
262
+ ```
263
+
264
+ ## Agent setup
265
+
266
+ Batchor keeps contributor tooling separate from the tools intended for researchers and downstream projects:
253
267
 
254
- - repo-local skill: `.agents/skills/batchor-dev/`
255
- - repo-local plugin marketplace: `.agents/plugins/marketplace.json`
256
- - repo-local MCP plugin: `plugins/batchor-agent-tools/`
257
- - VS Code workspace MCP config: `.vscode/mcp.json`
268
+ - `plugins/batchor/` is the user-facing Codex plugin. Its `$use-batchor` skill and MCP helpers turn datasets and prompts into safe CLI or Python workflows without assuming a Batchor source checkout.
269
+ - `.agents/skills/batchor-dev/` and `plugins/batchor-agent-tools/` are contributor-only. They teach agents how to change this repository, find design docs, and run its validation suite.
270
+ - `.agents/plugins/marketplace.json` registers both plugins for local development.
271
+ - `.vscode/mcp.json` configures the contributor MCP for this workspace.
258
272
 
259
- The skill captures repo-specific workflow and validation guidance. The MCP plugin exposes a small repo-aware guide for project overview, docs entry points, and validation commands.
273
+ The PyPI package remains the runtime dependency: `pip install batchor` installs the library and CLI. Agent skills and MCP configuration are distributed as the separate `batchor` plugin so Python environments do not receive Codex-specific files implicitly.
274
+
275
+ To try the user plugin from a local checkout:
276
+
277
+ ```bash
278
+ codex plugin marketplace add /path/to/batchor
279
+ codex plugin add batchor@batchor-local
280
+ ```
281
+
282
+ Start a new Codex task after installation, then ask: `Use $use-batchor to turn my CSV and research prompt into a resumable batch job.`
260
283
 
261
284
  Supported Python versions:
262
285
 
@@ -268,8 +291,9 @@ Supported Python versions:
268
291
 
269
292
  For Python API usage, auth resolution is:
270
293
 
271
- 1. explicit `OpenAIProviderConfig(api_key=...)`
272
- 2. ambient `OPENAI_API_KEY`
294
+ 1. explicit provider config credentials such as `OpenAIProviderConfig(api_key=...)` or `GeminiProviderConfig(api_key=...)`
295
+ 2. ambient provider environment variables, currently `OPENAI_API_KEY` or `GEMINI_API_KEY`
296
+ 3. Vertex AI Application Default Credentials when `GeminiProviderConfig(vertexai=True, ...)` or `GOOGLE_GENAI_USE_VERTEXAI=true` is used
273
297
 
274
298
  The Python library does not auto-load `.env`.
275
299
 
@@ -298,6 +322,43 @@ run = runner.run_and_wait(
298
322
  print(run.results()[0].output_text)
299
323
  ```
300
324
 
325
+ ### Gemini text job
326
+
327
+ ```python
328
+ from batchor import BatchItem, BatchJob, BatchRunner, GeminiProviderConfig, PromptParts
329
+
330
+
331
+ runner = BatchRunner(storage="memory")
332
+ run = runner.run_and_wait(
333
+ BatchJob(
334
+ items=[BatchItem(item_id="row1", payload="Summarize this text")],
335
+ build_prompt=lambda item: PromptParts(prompt=item.payload),
336
+ provider_config=GeminiProviderConfig(
337
+ model="gemini-2.5-flash",
338
+ api_key="YOUR_GEMINI_API_KEY",
339
+ ),
340
+ )
341
+ )
342
+
343
+ print(run.results()[0].output_text)
344
+ ```
345
+
346
+ Gemini support currently builds text-only `GenerateContent` batch requests. It uses Gemini JSONL `key` values internally while keeping `batchor`'s durable item and attempt tracking unchanged.
347
+
348
+ For Vertex AI, provide a Cloud Storage staging prefix and use Application Default Credentials:
349
+
350
+ ```python
351
+ provider_config = GeminiProviderConfig(
352
+ model="gemini-2.5-flash",
353
+ vertexai=True,
354
+ project="my-project",
355
+ location="europe-west8",
356
+ gcs_uri="gs://my-bucket/batchor",
357
+ )
358
+ ```
359
+
360
+ Vertex AI stages JSONL input and output in that prefix. The Gemini Developer API uses inline requests for batches below 20 MB and the Files API for larger batches; `input_mode=` can override that automatic choice. Vertex request/output correlation uses a generated request label because Vertex output does not include the Developer API JSONL `key`.
361
+
301
362
  ### Structured output
302
363
 
303
364
  ```python
@@ -351,6 +412,8 @@ Structured-output models are validated up front against the OpenAI strict-schema
351
412
 
352
413
  If you need a field to be optional in Python, model it as nullable in the schema shape OpenAI accepts rather than relying on omitted required fields.
353
414
 
415
+ The same `structured_output=` API is available with `GeminiProviderConfig`; batchor sends the schema through Gemini `generation_config.response_json_schema` and validates the returned JSON text with the same Pydantic model.
416
+
354
417
  ### Rehydrate a durable run
355
418
 
356
419
  ```python
@@ -498,6 +561,31 @@ The CLI is intentionally narrower than the Python API:
498
561
  - CSV and JSONL only
499
562
  - SQLite-backed durable runs only
500
563
 
564
+ OpenAI remains the default provider. For Gemini, install `batchor[gemini]` and select the backend explicitly or let `auto` follow `GOOGLE_GENAI_USE_VERTEXAI`:
565
+
566
+ ```bash
567
+ batchor start \
568
+ --input input/items.jsonl \
569
+ --id-field id \
570
+ --prompt-field text \
571
+ --provider gemini \
572
+ --model gemini-2.5-flash \
573
+ --gemini-backend developer
574
+ ```
575
+
576
+ Vertex AI additionally needs a writable staging prefix:
577
+
578
+ ```bash
579
+ batchor start \
580
+ --input input/items.jsonl \
581
+ --id-field id \
582
+ --prompt-field text \
583
+ --provider gemini \
584
+ --model gemini-2.5-flash \
585
+ --gemini-backend vertex \
586
+ --gcs-uri gs://my-bucket/batchor
587
+ ```
588
+
501
589
  Start a run from JSONL:
502
590
 
503
591
  ```bash
@@ -2,7 +2,7 @@
2
2
 
3
3
  `batchor` grew out of a recurring problem in academic research: running large datasets through LLMs in batch — reliably, reproducibly, and without reinventing the same glue code across every project. The patterns that kept emerging (durable state, typed results, safe resume after failure) were extracted into this library so they do not have to be rebuilt each time.
4
4
 
5
- `batchor` is a durable OpenAI Batch runner for Python teams that want:
5
+ `batchor` is a durable provider Batch runner for Python teams that want:
6
6
 
7
7
  - typed Pydantic results
8
8
  - resumable durable runs
@@ -12,7 +12,7 @@
12
12
  - library-first run controls
13
13
  - a small operator CLI for CSV and JSONL jobs
14
14
 
15
- It is intentionally narrow today: OpenAI-first, SQLite-first, and library-first.
15
+ It is intentionally narrow today: OpenAI is the CLI default, Gemini is opt-in through `batchor[gemini]`, SQLite is the CLI durability backend, and the Python API exposes the broadest configuration surface.
16
16
 
17
17
  ## What problem it solves
18
18
 
@@ -37,6 +37,7 @@ Most OpenAI Batch examples stop at "upload a JSONL file and poll until it finish
37
37
  Built-in implementations:
38
38
 
39
39
  - `OpenAIProviderConfig` + `OpenAIBatchProvider`
40
+ - `GeminiProviderConfig` + `GeminiBatchProvider` for text-only Gemini Batch jobs
40
41
  - `SQLiteStorage`
41
42
  - `PostgresStorage` as an opt-in durable control-plane backend
42
43
  - `MemoryStateStore`
@@ -52,6 +53,8 @@ Important constraints:
52
53
  - the CLI supports file-backed inputs only
53
54
  - users still own selecting and ordering input files or partitions
54
55
  - the built-in CLI uses SQLite durability only
56
+ - the CLI supports OpenAI plus Gemini Developer API and Vertex AI text jobs
57
+ - Gemini support is text-only for now and does not build multimodal requests
55
58
  - structured-output rehydration requires an importable module-level Pydantic model
56
59
  - raw output artifacts are retained by default and must be exported before raw pruning
57
60
  - pause/resume/cancel and incremental terminal-result APIs are library-first today
@@ -90,6 +93,7 @@ graph LR
90
93
 
91
94
  subgraph providers["providers/"]
92
95
  OpenAI["OpenAIBatchProvider"]
96
+ Gemini["GeminiBatchProvider"]
93
97
  end
94
98
 
95
99
  subgraph sources["sources/"]
@@ -109,6 +113,7 @@ graph LR
109
113
  User -->|"start() / run_and_wait()"| BatchRunner
110
114
  BatchRunner --> Run
111
115
  BatchRunner --> OpenAI
116
+ BatchRunner --> Gemini
112
117
  BatchRunner --> SQLite
113
118
  BatchRunner --> LocalFS
114
119
  Files -->|"BatchItem stream"| BatchRunner
@@ -144,16 +149,31 @@ Operational semantics for resume, run control, and artifact retention live in
144
149
  pip install batchor
145
150
  ```
146
151
 
147
- ## Repo Agent Setup
152
+ For Gemini Batch support, install the optional extra:
148
153
 
149
- This repo now includes local AI-agent scaffolding so a contributor agent can pick up repo conventions without extra global setup:
154
+ ```bash
155
+ pip install "batchor[gemini]"
156
+ ```
157
+
158
+ ## Agent setup
159
+
160
+ Batchor keeps contributor tooling separate from the tools intended for researchers and downstream projects:
150
161
 
151
- - repo-local skill: `.agents/skills/batchor-dev/`
152
- - repo-local plugin marketplace: `.agents/plugins/marketplace.json`
153
- - repo-local MCP plugin: `plugins/batchor-agent-tools/`
154
- - VS Code workspace MCP config: `.vscode/mcp.json`
162
+ - `plugins/batchor/` is the user-facing Codex plugin. Its `$use-batchor` skill and MCP helpers turn datasets and prompts into safe CLI or Python workflows without assuming a Batchor source checkout.
163
+ - `.agents/skills/batchor-dev/` and `plugins/batchor-agent-tools/` are contributor-only. They teach agents how to change this repository, find design docs, and run its validation suite.
164
+ - `.agents/plugins/marketplace.json` registers both plugins for local development.
165
+ - `.vscode/mcp.json` configures the contributor MCP for this workspace.
155
166
 
156
- The skill captures repo-specific workflow and validation guidance. The MCP plugin exposes a small repo-aware guide for project overview, docs entry points, and validation commands.
167
+ The PyPI package remains the runtime dependency: `pip install batchor` installs the library and CLI. Agent skills and MCP configuration are distributed as the separate `batchor` plugin so Python environments do not receive Codex-specific files implicitly.
168
+
169
+ To try the user plugin from a local checkout:
170
+
171
+ ```bash
172
+ codex plugin marketplace add /path/to/batchor
173
+ codex plugin add batchor@batchor-local
174
+ ```
175
+
176
+ Start a new Codex task after installation, then ask: `Use $use-batchor to turn my CSV and research prompt into a resumable batch job.`
157
177
 
158
178
  Supported Python versions:
159
179
 
@@ -165,8 +185,9 @@ Supported Python versions:
165
185
 
166
186
  For Python API usage, auth resolution is:
167
187
 
168
- 1. explicit `OpenAIProviderConfig(api_key=...)`
169
- 2. ambient `OPENAI_API_KEY`
188
+ 1. explicit provider config credentials such as `OpenAIProviderConfig(api_key=...)` or `GeminiProviderConfig(api_key=...)`
189
+ 2. ambient provider environment variables, currently `OPENAI_API_KEY` or `GEMINI_API_KEY`
190
+ 3. Vertex AI Application Default Credentials when `GeminiProviderConfig(vertexai=True, ...)` or `GOOGLE_GENAI_USE_VERTEXAI=true` is used
170
191
 
171
192
  The Python library does not auto-load `.env`.
172
193
 
@@ -195,6 +216,43 @@ run = runner.run_and_wait(
195
216
  print(run.results()[0].output_text)
196
217
  ```
197
218
 
219
+ ### Gemini text job
220
+
221
+ ```python
222
+ from batchor import BatchItem, BatchJob, BatchRunner, GeminiProviderConfig, PromptParts
223
+
224
+
225
+ runner = BatchRunner(storage="memory")
226
+ run = runner.run_and_wait(
227
+ BatchJob(
228
+ items=[BatchItem(item_id="row1", payload="Summarize this text")],
229
+ build_prompt=lambda item: PromptParts(prompt=item.payload),
230
+ provider_config=GeminiProviderConfig(
231
+ model="gemini-2.5-flash",
232
+ api_key="YOUR_GEMINI_API_KEY",
233
+ ),
234
+ )
235
+ )
236
+
237
+ print(run.results()[0].output_text)
238
+ ```
239
+
240
+ Gemini support currently builds text-only `GenerateContent` batch requests. It uses Gemini JSONL `key` values internally while keeping `batchor`'s durable item and attempt tracking unchanged.
241
+
242
+ For Vertex AI, provide a Cloud Storage staging prefix and use Application Default Credentials:
243
+
244
+ ```python
245
+ provider_config = GeminiProviderConfig(
246
+ model="gemini-2.5-flash",
247
+ vertexai=True,
248
+ project="my-project",
249
+ location="europe-west8",
250
+ gcs_uri="gs://my-bucket/batchor",
251
+ )
252
+ ```
253
+
254
+ Vertex AI stages JSONL input and output in that prefix. The Gemini Developer API uses inline requests for batches below 20 MB and the Files API for larger batches; `input_mode=` can override that automatic choice. Vertex request/output correlation uses a generated request label because Vertex output does not include the Developer API JSONL `key`.
255
+
198
256
  ### Structured output
199
257
 
200
258
  ```python
@@ -248,6 +306,8 @@ Structured-output models are validated up front against the OpenAI strict-schema
248
306
 
249
307
  If you need a field to be optional in Python, model it as nullable in the schema shape OpenAI accepts rather than relying on omitted required fields.
250
308
 
309
+ The same `structured_output=` API is available with `GeminiProviderConfig`; batchor sends the schema through Gemini `generation_config.response_json_schema` and validates the returned JSON text with the same Pydantic model.
310
+
251
311
  ### Rehydrate a durable run
252
312
 
253
313
  ```python
@@ -395,6 +455,31 @@ The CLI is intentionally narrower than the Python API:
395
455
  - CSV and JSONL only
396
456
  - SQLite-backed durable runs only
397
457
 
458
+ OpenAI remains the default provider. For Gemini, install `batchor[gemini]` and select the backend explicitly or let `auto` follow `GOOGLE_GENAI_USE_VERTEXAI`:
459
+
460
+ ```bash
461
+ batchor start \
462
+ --input input/items.jsonl \
463
+ --id-field id \
464
+ --prompt-field text \
465
+ --provider gemini \
466
+ --model gemini-2.5-flash \
467
+ --gemini-backend developer
468
+ ```
469
+
470
+ Vertex AI additionally needs a writable staging prefix:
471
+
472
+ ```bash
473
+ batchor start \
474
+ --input input/items.jsonl \
475
+ --id-field id \
476
+ --prompt-field text \
477
+ --provider gemini \
478
+ --model gemini-2.5-flash \
479
+ --gemini-backend vertex \
480
+ --gcs-uri gs://my-bucket/batchor
481
+ ```
482
+
398
483
  Start a run from JSONL:
399
484
 
400
485
  ```bash
@@ -11,6 +11,8 @@ batchor/
11
11
  .agents/
12
12
  docs/
13
13
  plugins/
14
+ batchor/
15
+ batchor-agent-tools/
14
16
  src/batchor/
15
17
  artifacts/
16
18
  cli.py
@@ -22,13 +24,14 @@ batchor/
22
24
  tests/
23
25
  ```
24
26
 
25
- Repo-local contributor tooling also lives outside the shipped Python package:
27
+ Agent tooling lives outside the shipped Python package so Python installation does not mutate or assume a particular coding-agent environment:
26
28
 
27
29
  - `.agents/skills/batchor-dev/` contains the repo skill for AI-agent onboarding
28
30
  - `.agents/plugins/marketplace.json` registers repo-local plugins for Codex-style discovery
29
- - `plugins/batchor-agent-tools/` contains the repo-local MCP server and plugin manifest
31
+ - `plugins/batchor-agent-tools/` contains contributor-only repo guidance and validation tools
32
+ - `plugins/batchor/` contains the downstream user skill and repo-independent workflow MCP for researchers and agents integrating Batchor into other projects
30
33
 
31
- These directories are contributor tooling only and are not part of the published `src/batchor` package.
34
+ These directories are not part of the published `src/batchor` package. PyPI distributes the runtime and CLI; the agent marketplace distributes agent-specific skills and MCP configuration.
32
35
 
33
36
  The package is organized around one core concern: durable batch execution. Most modules exist to support one of five responsibilities:
34
37
 
@@ -58,6 +61,7 @@ graph TB
58
61
  subgraph providers["providers/"]
59
62
  BatchProvider["BatchProvider (ABC)"]
60
63
  OpenAIProvider["OpenAIBatchProvider"]
64
+ GeminiProvider["GeminiBatchProvider"]
61
65
  ProviderRegistry
62
66
  end
63
67
 
@@ -91,6 +95,7 @@ graph TB
91
95
  MemoryStateStore -.->|implements| StateStore
92
96
  BatchRunner -->|submits/polls| BatchProvider
93
97
  OpenAIProvider -.->|implements| BatchProvider
98
+ GeminiProvider -.->|implements| BatchProvider
94
99
  BatchRunner -->|stores artifacts| ArtifactStore
95
100
  LocalArtifactStore -.->|implements| ArtifactStore
96
101
  BatchRunner -->|creates via| ProviderRegistry
@@ -120,6 +125,10 @@ The public runtime model centers on four types:
120
125
  one logical source, while callers remain responsible for selecting and ordering
121
126
  the child sources up front.
122
127
 
128
+ Provider adaptation is intentionally concentrated behind `BatchProvider`.
129
+ The runtime stores one durable internal custom identifier per item attempt, while each provider maps that identifier to its own request shape. OpenAI uses `custom_id`; the Gemini Developer API uses `key`; Vertex AI uses a request label that is returned with the original request in GCS output.
130
+ Provider hooks also own response-text extraction so structured-output parsing can stay generic across provider payload shapes.
131
+
123
132
  ## Main user-facing flow
124
133
 
125
134
  The normal public flow is:
@@ -137,7 +146,7 @@ Internally that expands to:
137
146
  4. Claim a bounded submission window from pending items.
138
147
  5. Build or replay request JSONL rows.
139
148
  6. Persist request artifacts before upload.
140
- 7. Submit one or more OpenAI batch files.
149
+ 7. Submit one or more provider batch files.
141
150
  8. Poll active batches.
142
151
  9. Download output/error files.
143
152
  10. Parse terminal item results back into the state store.
@@ -201,7 +210,7 @@ sequenceDiagram
201
210
  BatchRunner->>Provider: upload_input_file(local_path)
202
211
  Provider-->>BatchRunner: remote_file_id
203
212
  BatchRunner->>Provider: create_batch(remote_file_id)
204
- Provider-->>BatchRunner: BatchRemoteRecord (status=validating)
213
+ Provider-->>BatchRunner: BatchRemoteRecord (status=submitted/validating)
205
214
  BatchRunner->>StateStore: register_batch()
206
215
  BatchRunner->>StateStore: mark_items_submitted()
207
216
  end
@@ -423,7 +432,7 @@ That split gives `batchor`:
423
432
  ## Current invariants
424
433
 
425
434
  1. Public execution is run-oriented: `start()`, `get_run()`, `run_and_wait()`.
426
- 2. OpenAI Batch is the only built-in provider.
435
+ 2. OpenAI Batch and text-only Gemini Batch are built-in Python and CLI providers; OpenAI remains the CLI default.
427
436
  3. SQLite is the default durable backend.
428
437
  4. Postgres is an opt-in durable backend for shared control-plane state.
429
438
  5. Structured outputs require a module-level Pydantic v2 model for rehydration.
@@ -456,7 +465,7 @@ The code is shaped for future providers and backends, but within explicit bounda
456
465
 
457
466
  ## Current gaps
458
467
 
459
- - the only built-in provider is OpenAI Batch
468
+ - Gemini request construction is text-only
460
469
  - the only built-in artifact backend is local filesystem storage
461
470
  - arbitrary non-checkpointable iterables do not support mid-ingest crash recovery
462
471
  - the CLI does not expose the full Python API surface
@@ -0,0 +1,96 @@
1
+ # Gemini Batching
2
+
3
+ This document describes the Gemini-specific behavior inside `batchor`.
4
+
5
+ Status: implemented for text-only Python API and CLI jobs through both the Gemini Developer API and Vertex AI. Multimodal request construction remains out of scope.
6
+
7
+ ## API split
8
+
9
+ The two Google backends do not expose the same batch transport:
10
+
11
+ | Backend | Input | Output | Authentication |
12
+ | --- | --- | --- | --- |
13
+ | Gemini Developer API | inline requests below 20 MB; Files API JSONL for larger batches | inline responses or a Files API JSONL file | API key |
14
+ | Vertex AI | Cloud Storage JSONL (BigQuery is supported by Google but not by batchor) | Cloud Storage JSONL | Application Default Credentials |
15
+
16
+ `GeminiProviderConfig.input_mode="auto"` follows that table. `inline`, `file`, and `gcs` are explicit overrides, and incompatible backend/mode combinations fail locally.
17
+
18
+ Vertex selection is explicit through `vertexai=True` or ambient through `GOOGLE_GENAI_USE_VERTEXAI=true`. Project and location resolve from config first, then `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`. Vertex also requires `gcs_uri="gs://bucket/prefix"` for request and output staging.
19
+
20
+ ## Request construction and correlation
21
+
22
+ Developer file requests use the documented JSONL shape:
23
+
24
+ ```json
25
+ {"key":"row1:a1","request":{"contents":[{"role":"user","parts":[{"text":"Summarize this text"}]}]}}
26
+ ```
27
+
28
+ Inline mode converts those durable request artifacts to SDK `InlinedRequest` objects and carries the key in request metadata. Returned inline metadata is converted back to the same JSONL-style record before the generic runtime consumes it.
29
+
30
+ Vertex output returns the original `request`, a `response`, and a string `status`; it does not return the Developer API `key`. For Vertex, batchor hashes the internal attempt identifier into a GCP-safe `request.labels.batchor_key` value. The original request and label round-trip through GCS output, so the runtime can correlate each row without relying on output ordering.
31
+
32
+ `PromptParts.prompt` becomes a user text part. `PromptParts.system_prompt` becomes `system_instruction`. `GeminiProviderConfig.generation_config` is copied into every request.
33
+
34
+ Structured jobs additionally set:
35
+
36
+ ```json
37
+ {
38
+ "generation_config": {
39
+ "response_mime_type": "application/json",
40
+ "response_json_schema": {"type": "object"}
41
+ }
42
+ }
43
+ ```
44
+
45
+ The schema comes from the same `structured_output=` Pydantic model used by OpenAI jobs and returned JSON is validated with that model.
46
+
47
+ ## Lifecycle
48
+
49
+ Developer API auto mode:
50
+
51
+ 1. use inline requests when the staged JSONL is under 20 MB, otherwise upload it through the Files API
52
+ 2. create and poll the batch with `client.batches`
53
+ 3. normalize inline responses or download the result file
54
+
55
+ Vertex AI:
56
+
57
+ 1. upload the durable request JSONL under `gcs_uri/inputs/`
58
+ 2. create a batch with that `gs://` source and a unique `gcs_uri/outputs/` destination
59
+ 3. poll with `client.batches.get`
60
+ 4. read every output object from the returned `output_info.gcs_output_directory`
61
+
62
+ Terminal states normalize to batchor's generic `completed`, `failed`, `cancelled`, and `expired` states. Vertex rows with a non-empty `status` become item errors; rows with an object `response` and empty status become successes.
63
+
64
+ ## Durable replay
65
+
66
+ Provider-specific request rows remain in batchor's local artifact store. Retry or fresh-process resume replaces only the provider correlation identifier. Developer rows update `key`; Vertex rows update the hashed correlation label. The prompt and generation request otherwise remain unchanged.
67
+
68
+ GCS input/output objects are remote provider payloads, separate from batchor's durable local artifact store. A failed create deletes its just-uploaded input on a best-effort basis. Successfully submitted Vertex payloads follow the bucket's retention/lifecycle policy.
69
+
70
+ ## Limits verified July 10, 2026
71
+
72
+ Current official Vertex behavior:
73
+
74
+ - no predefined Gemini batch inference quota or enqueue-token limit; work can queue against a shared resource pool
75
+ - at most 200,000 requests per job
76
+ - at most 1 GB for a Cloud Storage input file
77
+ - jobs can queue for up to 72 hours and generally process within 24 hours after starting
78
+
79
+ Batchor therefore does not apply OpenAI enqueue-token budgeting to Gemini. Generic `ChunkPolicy` defaults remain more conservative at 50,000 requests and 150 MiB, and callers may raise them within Google's limits.
80
+
81
+ Developer API limits differ: inline batch creation must stay below 20 MB, input files may be up to 2 GB, concurrent batches are currently limited to 100, and model/tier-specific enqueued-token limits still apply. Batchor automatically switches Developer auto mode from inline to Files API by request size; it does not yet model Developer tier-specific token quotas.
82
+
83
+ ## Current limits
84
+
85
+ - the CLI exposes the common text-job configuration; advanced source, storage, and lifecycle composition remains Python-first.
86
+ - text request construction only.
87
+ - Vertex BigQuery input/output is not implemented.
88
+ - provider-side remote cancellation is not implemented.
89
+
90
+ ## References
91
+
92
+ - [Vertex AI Gemini batch inference](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/batch-prediction-gemini)
93
+ - [Vertex AI batch inference from Cloud Storage](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/batch-prediction-from-cloud-storage)
94
+ - [Vertex AI quotas and limits](https://cloud.google.com/vertex-ai/docs/quotas)
95
+ - [Gemini Developer API Batch API](https://ai.google.dev/gemini-api/docs/batch-api)
96
+ - [Google Gen AI Python SDK batch documentation](https://googleapis.github.io/python-genai/#batch-prediction)
@@ -2,10 +2,12 @@
2
2
 
3
3
  This document describes the OpenAI-specific behavior inside `batchor`.
4
4
 
5
- `batchor` is not a generic batch abstraction with many first-party providers yet. The OpenAI path is the primary implemented path, so a lot of runtime behavior is defined around OpenAI Batch semantics.
5
+ OpenAI remains the default and most feature-complete provider path. Gemini has its own design note in [`GEMINI_BATCHING.md`](GEMINI_BATCHING.md), while this page focuses only on OpenAI Batch semantics.
6
6
 
7
7
  ## Current behavior
8
8
 
9
+ The Python API and CLI both support OpenAI. The CLI is OpenAI-only today.
10
+
9
11
  ## Request construction
10
12
 
11
13
  The built-in provider converts each prepared item into one OpenAI Batch JSONL request row.
@@ -187,7 +189,6 @@ Provider-side remote batch cancellation is not implemented in v1.
187
189
 
188
190
  ## Current limits
189
191
 
190
- - only the built-in OpenAI Batch provider path is implemented
191
192
  - the docs do not yet provide a full capability matrix across all OpenAI endpoint features
192
193
  - artifact storage is still local-filesystem-only
193
194
 
@@ -8,11 +8,12 @@ This file tracks important work that should be explicit in the extracted reposit
8
8
  - extend resumable ingestion beyond the built-in CSV/JSONL sources
9
9
  - add automated retention windows on top of the explicit export/prune lifecycle
10
10
  - add more input adapters beyond CSV and JSONL
11
+ - expose non-OpenAI providers through CLI workflows once provider-specific auth and flags are stable
11
12
  - add CLI structured-output workflows if the importability story can stay durable and predictable
12
13
 
13
14
  ## Longer-Term Ideas
14
15
 
15
- - additional providers beyond OpenAI
16
+ - additional provider coverage beyond OpenAI and text-only Gemini
16
17
  - artifact store abstraction
17
18
  - partial-result streaming APIs
18
19
  - richer CLI or operator workflow beyond the current file-backed text-job surface
@@ -74,7 +74,7 @@ Postgres is also implemented for shared control-plane state when callers explici
74
74
 
75
75
  In-memory storage exists for tests and short-lived local runs.
76
76
 
77
- The SQLite/OpenAI path is covered by the default smoke test. Postgres storage compatibility is validated in a dedicated storage-contract CI job and requires `BATCHOR_TEST_POSTGRES_DSN` for equivalent local coverage.
77
+ The SQLite/OpenAI path is covered by the default smoke test. Gemini provider wiring is covered with fake-client integration tests, and an opt-in live Vertex/GCS smoke test covers the real control and payload planes. Postgres storage compatibility is validated in a dedicated storage-contract CI job and requires `BATCHOR_TEST_POSTGRES_DSN` for equivalent local coverage.
78
78
 
79
79
  ## Storage responsibilities
80
80
 
@@ -179,7 +179,7 @@ Successful rehydration depends on:
179
179
  Fresh-process resume also requeues any `queued_local` items back to `pending` before submission resumes.
180
180
  When `start(job, run_id=...)` resumes a run with active provider batches, it first performs a poll-only reconciliation pass. Terminal provider batches are consumed, failed batches update retry backoff, and any active backoff prevents new source materialization or submission until the backoff expires.
181
181
 
182
- Resume compatibility intentionally ignores non-persisted secret fields such as provider API keys.
182
+ Resume compatibility intentionally ignores non-persisted secret fields such as provider API keys. Rehydrated OpenAI runs need `OPENAI_API_KEY` when no explicit in-memory config is supplied. Gemini Developer API runs need `GEMINI_API_KEY`; Vertex runs need Application Default Credentials plus the persisted or ambient project, location, and GCS prefix configuration.
183
183
 
184
184
  For deterministic-source resume, the caller must also reuse the same `run_id` and provide the same source identity/fingerprint.
185
185
  For composite sources, that includes the same ordered child identities; changing the child order or swapping one file changes the logical source identity.
@@ -194,6 +194,7 @@ Built-in deterministic sources currently include:
194
194
  For checkpointed ingestion, durable stores append materialized item chunks and advance the ingest checkpoint in one storage operation. Parquet and composite sources can also identify an end-of-source checkpoint from cheap metadata, so a resume after all rows were materialized but before the final completion flag was written can mark ingestion complete without re-reading the source data or re-rendering prompts.
195
195
 
196
196
  Once an item has a durable request artifact pointer, `batchor` prunes large inline request-building fields from the control-plane store and relies on the artifact for later retries.
197
+ Provider-specific request rows are preserved in those artifacts. During replay, OpenAI updates `custom_id`; Gemini updates either the Developer API `key` or the Vertex correlation label. All map back to the same internal durable attempt identifier.
197
198
 
198
199
  ## Artifact lifecycle
199
200