Conversation
…tore A provider-native code execution keeps what it wrote in the provider's own container and answers with the provider's file id. #1366 recorded that as a `file_objects` row with no bytes and streamed them from the provider on demand, which works right up until the provider stops holding them. OpenAI discards a container twenty minutes after its last use, and everything in it, so a caller returning to the conversation the next day held an id for a chart nobody could serve, and `GET /v1/files/{id}/content` answered 502. The same row was invisible to the content normalizer, which skips a file with no `storage_ref`, so the chart could not be handed back to a model either. The bytes are copied into Otari's store as the run is recorded, so a produced file is an ordinary Otari file: it downloads after the container is gone, and it can be attached to a later turn. The provider's id stays the row id, so a client echoing the turn back still names a file Otari knows. The copy streams rather than buffering, and takes the two caps a sandbox run's outputs already get: `files_output_max_files` bounds how many are copied, `files_output_max_bytes` how many bytes across the response, and a file past either is still recorded so its id resolves. Copying is best effort, which is what decides the shape of the rest. A copy that fails leaves the row exactly as it was before this change: no `storage_ref`, the provider named, the bytes proxied on demand. So the proxy stays rather than being removed as the issue suggested, because deployments already hold rows written before copying existed and removing it would break them. The download route now prefers a local copy and falls back to the proxy, rather than treating any provider row as remote. Two things the copy needed that were not there. It streams into the store the app built, threaded through `RequestContext`, rather than building one from config: those agree in production but not where a store is injected, and bytes written to the wrong store are bytes the download route cannot find. And a failed copy discards its partial blob, so a proxy row never shadows a truncated one. Timing, which the issue left open: the copy runs where recording already runs. The streaming path records after the stream settles, so it costs nothing there. The non-streaming path records before the reply returns, so it adds the copy to the reply, bounded by the two caps. Inline is what makes the file present the moment its id is announced. Closes #1475 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
daavoo
requested review from
a team,
peteski22 and
tbille
and removed request for
a team
September 22, 2026 16:08
3 tasks
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: Repository: mozilla-ai/otari/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Contributor
|
Same PR/Duplicate of #1517 |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Closes #1475.
A provider-native code execution keeps what it wrote in the provider's own container and answers with the provider's file id. #1366 recorded that as a
file_objectsrow with no bytes and streamed them from the provider on demand, which works right up until the provider stops holding them. OpenAI discards a container twenty minutes after its last use, together with everything in it, so a caller who comes back to the conversation the next day holds an id for a chart nobody can serve andGET /v1/files/{id}/contentanswers 502. The same row is invisible to the content normalizer, which skips a file with nostorage_ref, so the chart cannot be handed back to a model either.This copies the bytes into Otari's store as the run is recorded. A produced file becomes an ordinary Otari file: it downloads after the container is gone, and it can be attached to a later turn. The provider's id stays the row id, so a client echoing the turn back still names a file Otari knows.
What a reviewer should know
storage_ref, the provider named, the bytes proxied on demand. Losing a copy must not cost the caller the response it already has.files_output_max_filesbounds how many files are copied per response andfiles_output_max_byteshow many bytes across it. A file past either is still recorded, so its id resolves; it just serves by proxy. The byte count is checked as the chunks arrive rather than fromContent-Length, because what a run writes is untrusted.RequestContext, rather than building one from config. Those agree in production and not where a store is injected, and bytes written to the wrong store are bytes the download route cannot find. This was a real bug in the first draft, caught by a test.Timing
The issue left this open. The copy runs where recording already runs. The streaming path records after the stream settles, so it costs nothing there. The non-streaming path records before the reply returns, so the copy is added to the reply, bounded by the two caps above. Inline is what makes the file present the moment its id is announced; a background copy would leave a window where the id is public and the bytes are not, and the fallback during that window would be the proxy this change exists to stop relying on.
How to test it locally
Point a deployment at a provider that runs code natively, ask for a chart, and note the
file_idin the reply.GET /api/v1/files/{id}shows a non-zerobytes, which a proxied row never had. Wait past the provider's container lifetime (twenty minutes on OpenAI) andGET /api/v1/files/{id}/contentstill serves the chart. Attaching the same id to a later request now reaches the model rather than being skipped.What automated checks cover
Four tests against the issue's acceptance criteria, in
tests/integration/test_provider_file_download.py: a copied file still downloads once the provider refuses it, a produced file attached to a later request reaches the model, the count cap leaves the file past it recorded but uncopied, and a failed copy still serves by proxy. The existing two recording tests now assert the copy landed rather than assertingstorage_ref is None; their fixture fakes the provider's content call, without which a copy would fail the way a real outage does and those assertions would pass for the wrong reason.make lint,make typecheck,make openapi-checkandmake postman-checkpass, as do the files, content-normalizer and provider-produced-file suites (83 tests) and the wider integration slice matching file/sandbox/code_execution/messages (269 tests).PR Type
Relevant issues
Closes #1475. Part of #1470.
Checklist
tests/unit,tests/integration).make lint,make typecheck,make test).uv run python scripts/generate_openapi.py).ARCHITECTURE.mdorscripts/check_architecture.py, the description names the rule and says why.AI Usage
AI Model/Tool used: Claude Code
Any additional AI details you'd like to share:
Two things were found by writing the tests rather than by reading the code. The download route branched on
record.provider is not Nonebefore looking atstorage_ref, so a copied file was still fetched from the provider and the whole point of the change was lost; that is why the branch is now ordered on the copy. Andrecord_provider_filesbuilt its ownFileStorefrom config, which passes in production and writes to a store the route never reads in any deployment that injects one.🤖 Generated with Claude Code
Summary
Provider-produced files are now copied into Otari’s file store while the run is recorded. This allows files to remain available after provider containers expire and supports their use in later requests.
The change:
RequestContext.