-
Notifications
You must be signed in to change notification settings - Fork 58
feat(tools): run provider-native code execution on the sandbox when the model has none, with Files API parity #1366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d6d4be1
feat(tools): add the code-execution vocabulary and its settings
daavoo d9e9a72
feat(files): add the fsspec store and the shared file helpers
daavoo 7a196d4
feat(files): serve both SDKs' Files APIs, with the schema and service…
daavoo 90cfea6
feat(tools): run provider-native code execution on the sandbox when t…
daavoo e7452c8
chore: regenerate the public artifacts and add the SDK plot demos
daavoo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
81 changes: 81 additions & 0 deletions
81
alembic/versions/f2a6c81d9b47_code_execution_executor_and_provider_files.py
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| """Add the workspace executor pin and let a file row stand for a provider-held file. | ||
|
|
||
| Three things this branch needs, in one revision because they land together: | ||
|
|
||
| - ``workspace_code_execution_policies.executor``, the workspace's pin on who | ||
| runs a provider-native code-execution declaration (``auto``, ``otari`` or | ||
| ``provider``), over the deployment's default and over the request's header. | ||
| No backfill: NULL is "no pin", the state every existing row is in, so a | ||
| deployment upgrading onto this revision keeps deciding exactly as it did. | ||
| - ``file_objects.provider``, ``file_objects.provider_instance`` and | ||
| ``file_objects.provider_container_id``, with ``storage_ref`` made nullable. A | ||
| provider-native code execution keeps what it produced in the provider's own | ||
| container, so the row records who may read that id (the provider only | ||
| authenticates the deployment's credential) and which provider, through which | ||
| configured instance, to fetch it from, with no local blob to point at. | ||
| - Three indexes on ``file_objects``. One on ``expires_at``, because the sweep | ||
| selects ``deleted_at IS NOT NULL OR expires_at < now`` and the existing | ||
| ``deleted_at`` index cannot serve the ``OR`` on its own, so without it every | ||
| tick scanned the table. Two composites for the paged listing, the tenant | ||
| predicates then the keyset its cursor pages on: | ||
| ``(user_id, workspace_id, created_at, id)`` for a keyed request, and | ||
| ``(user_id, created_at, id)`` for a master-key listing that names no | ||
| workspace, which the first cannot order. Without them every page sorts the | ||
| user's whole set. | ||
|
|
||
| The ``ADD COLUMN``s need no table rebuild (see ``a4d7f1c9e2b6``), but dropping | ||
| ``storage_ref``'s NOT NULL does on SQLite, which has no ``ALTER COLUMN``, so | ||
| that one statement goes through ``batch_alter_table``. | ||
|
|
||
| Revision ID: f2a6c81d9b47 | ||
| Revises: d5f8b2a4c6e9 | ||
| Create Date: 2026-09-21 | ||
| """ | ||
|
|
||
| from collections.abc import Sequence | ||
|
|
||
| import sqlalchemy as sa | ||
| from alembic import op | ||
|
|
||
| revision: str = "f2a6c81d9b47" | ||
| down_revision: str | Sequence[str] | None = "d5f8b2a4c6e9" | ||
| branch_labels: str | Sequence[str] | None = None | ||
| depends_on: str | Sequence[str] | None = None | ||
|
|
||
| _POLICY_TABLE = "workspace_code_execution_policies" | ||
| _FILES_TABLE = "file_objects" | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| op.add_column(_POLICY_TABLE, sa.Column("executor", sa.String(length=16), nullable=True)) | ||
|
|
||
| op.add_column(_FILES_TABLE, sa.Column("provider", sa.String(), nullable=True)) | ||
| op.add_column(_FILES_TABLE, sa.Column("provider_instance", sa.String(), nullable=True)) | ||
| op.add_column(_FILES_TABLE, sa.Column("provider_container_id", sa.String(), nullable=True)) | ||
| with op.batch_alter_table(_FILES_TABLE) as batch: | ||
| batch.alter_column("storage_ref", existing_type=sa.String(), nullable=True) | ||
|
|
||
| op.create_index("ix_file_objects_expires_at", _FILES_TABLE, ["expires_at"]) | ||
| op.create_index( | ||
| "ix_file_objects_user_workspace_created", | ||
| _FILES_TABLE, | ||
| ["user_id", "workspace_id", "created_at", "id"], | ||
| ) | ||
| op.create_index("ix_file_objects_user_created", _FILES_TABLE, ["user_id", "created_at", "id"]) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| op.drop_index("ix_file_objects_user_created", table_name=_FILES_TABLE) | ||
| op.drop_index("ix_file_objects_user_workspace_created", table_name=_FILES_TABLE) | ||
| op.drop_index("ix_file_objects_expires_at", table_name=_FILES_TABLE) | ||
|
|
||
| # A provider-held row has no blob to point at, so it cannot survive the | ||
| # column going back to NOT NULL. | ||
| op.execute(sa.text("DELETE FROM file_objects WHERE storage_ref IS NULL")) | ||
| with op.batch_alter_table(_FILES_TABLE) as batch: | ||
| batch.alter_column("storage_ref", existing_type=sa.String(), nullable=False) | ||
| op.drop_column(_FILES_TABLE, "provider_container_id") | ||
| op.drop_column(_FILES_TABLE, "provider_instance") | ||
| op.drop_column(_FILES_TABLE, "provider") | ||
|
|
||
| op.drop_column(_POLICY_TABLE, "executor") | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| .env | ||
| plots/ |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| #!/usr/bin/env python3 | ||
| """Anthropic's own SDK, pointed at Otari, asks for a bar plot and saves it. | ||
|
|
||
| The point is the model id. Nothing else in the script changes between: | ||
|
|
||
| uv run python demo/code-exec/plot_with_anthropic_sdk.py anthropic:claude-sonnet-4-6 | ||
| uv run python demo/code-exec/plot_with_anthropic_sdk.py nebius:openai/gpt-oss-120b | ||
|
|
||
| The first runs on Anthropic's own sandbox, because the `auto` executor leaves a | ||
| declaration with a provider that serves it natively. The second has no native | ||
| sandbox, so Otari runs the code. Either way the reply carries the same | ||
| `code_execution_tool_result` blocks, and the chart downloads from Otari's files | ||
| API with the same call: a file Otari's sandbox produced it stored, and one | ||
| Anthropic's produced it streams back from Anthropic under Anthropic's own id. | ||
|
|
||
| Environment: | ||
| OTARI_URL default http://localhost:8000 | ||
| OTARI_KEY the gateway's master key or an API key (default demo-master-key) | ||
| QA_USER the user a master-key request bills (default plot-roundtrip) | ||
| QA_OUT_DIR where to write the plots (default demo/code-exec/plots) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| import pathlib | ||
| import sys | ||
| from typing import Any | ||
|
|
||
| from anthropic import Anthropic | ||
|
|
||
| OTARI_URL = os.environ.get("OTARI_URL", "http://localhost:8000").rstrip("/") | ||
| OTARI_KEY = os.environ.get("OTARI_KEY", "demo-master-key") | ||
| USER = os.environ.get("QA_USER", "plot-roundtrip") | ||
| OUT_DIR = pathlib.Path(os.environ.get("QA_OUT_DIR", pathlib.Path(__file__).parent / "plots")) | ||
|
|
||
| PROMPT = ( | ||
| "Use the code execution tool to draw a bar chart of these sales figures with " | ||
| "matplotlib: Jan 120, Feb 95, Mar 160, Apr 210, May 175. Save it as 'bar_plot.png' " | ||
| "in the working directory, then reply with the file id of the png you saved." | ||
| ) | ||
| CODE_TOOL: Any = {"type": "code_execution_20250825", "name": "code_execution"} | ||
|
|
||
| # The SDK appends /v1 to its base URL, and Otari serves its API under /api/v1. | ||
| # `default_query` is only for a master key: the files routes want to be told | ||
| # whose files to read, which an API key answers by itself. | ||
| otari = Anthropic(base_url=f"{OTARI_URL}/api", api_key=OTARI_KEY, default_query={"user": USER}) | ||
|
|
||
|
|
||
| def produced_file_ids(message: Any) -> list[str]: | ||
| """Every file id the run's tool results announce, in the order they appear. | ||
|
|
||
| The python and the bash variants of the tool each name their outputs in | ||
| their own block type, and Otari answers in whichever the run used, so this | ||
| looks for the shape rather than for one block name. | ||
| """ | ||
| ids = [] | ||
| for block in message.content: | ||
| result = getattr(block, "content", None) | ||
| for output in getattr(result, "content", None) or []: | ||
| file_id = getattr(output, "file_id", None) | ||
| if file_id and file_id not in ids: | ||
| ids.append(file_id) | ||
| return ids | ||
|
|
||
|
|
||
| def download(file_id: str, model: str) -> pathlib.Path: | ||
| """Fetch a produced file from Otari, wherever its bytes actually live.""" | ||
| meta = otari.beta.files.retrieve_metadata(file_id) | ||
| body = otari.beta.files.download(file_id) | ||
|
|
||
| OUT_DIR.mkdir(parents=True, exist_ok=True) | ||
| path = OUT_DIR / f"{model.replace(':', '-').replace('/', '-')}-{meta.filename}" | ||
| body.write_to_file(path) | ||
| return path | ||
|
|
||
|
|
||
| def main(model: str) -> int: | ||
| # No `betas`: they are Anthropic's own, and a provider with no Messages API | ||
| # of its own cannot honor one, so Otari drops them rather than refusing. | ||
| message = otari.beta.messages.create( | ||
| model=model, | ||
| max_tokens=4096, | ||
| messages=[{"role": "user", "content": PROMPT}], | ||
| tools=[CODE_TOOL], | ||
| # A master-key request must say who it bills; an API key ignores this | ||
| # in favor of its own user. | ||
| metadata={"user_id": USER}, | ||
| ) | ||
|
|
||
| ran_natively = message.container is not None | ||
| print(f"{model}: code ran on {'the provider' if ran_natively else "otari's sandbox"}") | ||
|
|
||
| file_ids = produced_file_ids(message) | ||
| if not file_ids: | ||
| print("no file id in the tool results:") | ||
| print(message.model_dump_json(indent=2)[:2000]) | ||
| return 1 | ||
|
|
||
| for file_id in file_ids: | ||
| path = download(file_id, model) | ||
| print(f" {file_id} -> {path} ({path.stat().st_size} bytes)") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "anthropic:claude-sonnet-4-6")) |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| #!/usr/bin/env python3 | ||
| """any-llm, pointed at Otari, asks for a bar plot and saves it. | ||
|
|
||
| The third way to write the other two scripts. any-llm talks to Otari through | ||
| its `otari` provider, so one call shape reaches every model Otari serves and | ||
| the code-execution tool is declared in Otari's own vocabulary rather than a | ||
| provider's: | ||
|
|
||
| uv run python demo/code-exec/plot_with_any_llm.py nebius:openai/gpt-oss-120b | ||
| uv run python demo/code-exec/plot_with_any_llm.py anthropic:claude-sonnet-4-6 | ||
|
|
||
| `otari_code_execution` always runs on Otari's sandbox, whatever the model and | ||
| whatever the executor says, which is the trade for one vocabulary: an | ||
| `anthropic:` model here runs the code here rather than natively. Ask for the | ||
| provider's own sandbox with that provider's declaration instead, which is what | ||
| `plot_with_anthropic_sdk.py` does. | ||
|
|
||
| any-llm covers the inference half only. It has no files API, so the chart is | ||
| downloaded with a plain HTTP GET against the same gateway. | ||
|
|
||
| Environment: | ||
| OTARI_URL default http://localhost:8000 | ||
| OTARI_KEY the gateway's master key or an API key (default demo-master-key) | ||
| QA_USER the user a master-key request bills (default plot-roundtrip) | ||
| QA_OUT_DIR where to write the plots (default demo/code-exec/plots) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import os | ||
| import pathlib | ||
| import re | ||
| import sys | ||
| from typing import Any | ||
|
|
||
| import httpx | ||
| from any_llm import acompletion | ||
|
|
||
| OTARI_URL = os.environ.get("OTARI_URL", "http://localhost:8000").rstrip("/") | ||
| OTARI_KEY = os.environ.get("OTARI_KEY", "demo-master-key") | ||
| USER = os.environ.get("QA_USER", "plot-roundtrip") | ||
| OUT_DIR = pathlib.Path(os.environ.get("QA_OUT_DIR", pathlib.Path(__file__).parent / "plots")) | ||
|
|
||
| PROMPT = ( | ||
| "Use the code execution tool to draw a bar chart of these sales figures with " | ||
| "matplotlib: Jan 120, Feb 95, Mar 160, Apr 210, May 175. Save it as 'bar_plot.png' " | ||
| "in the working directory, then reply with the file id of the png you saved." | ||
| ) | ||
| # Otari's own declaration, the one shape that means the same thing to every model. | ||
| CODE_TOOL: Any = {"type": "otari_code_execution"} | ||
| # A stored file's id as the tool result gives it to the model. | ||
| FILE_ID = re.compile(r"file-[0-9a-f]{32}") | ||
|
|
||
| files = httpx.Client(base_url=f"{OTARI_URL}/api/v1", headers={"Otari-Key": OTARI_KEY}, timeout=60.0) | ||
|
|
||
|
|
||
| def save(file_id: str, model: str) -> pathlib.Path: | ||
| """Download one stored file and write it beside the other plots.""" | ||
| meta = files.get(f"/files/{file_id}", params={"user": USER}) | ||
| meta.raise_for_status() | ||
| body = files.get(f"/files/{file_id}/content", params={"user": USER}) | ||
| body.raise_for_status() | ||
|
|
||
| OUT_DIR.mkdir(parents=True, exist_ok=True) | ||
| path = OUT_DIR / f"{model.replace(':', '-').replace('/', '-')}-{meta.json()['filename']}" | ||
| path.write_bytes(body.content) | ||
| return path | ||
|
|
||
|
|
||
| async def run(model: str) -> int: | ||
| response = await acompletion( | ||
| # The gateway is the provider; the selector it routes on travels as the model. | ||
| provider="otari", | ||
| model=model, | ||
| messages=[{"role": "user", "content": PROMPT}], | ||
| tools=[CODE_TOOL], | ||
| api_base=f"{OTARI_URL}/api/v1", | ||
| api_key=OTARI_KEY, | ||
| # A master-key request must say who it bills; an API key ignores this | ||
| # in favor of its own user. | ||
| user=USER, | ||
| ) | ||
|
|
||
| # Chat Completions has no shape for a server-run tool, so the loop resolves | ||
| # inside the gateway and only the final message comes back. The model was | ||
| # given each produced file as `name (file_id: file-...)` and asked to pass | ||
| # the id on, which is what is read back here. | ||
| reply = response.choices[0].message.content or "" | ||
| file_ids = list(dict.fromkeys(FILE_ID.findall(reply))) | ||
| if not file_ids: | ||
| print(f"{model}: no stored file id in the reply: {reply[:300]}") | ||
| return 1 | ||
|
|
||
| print(f"{model}: code ran on otari's sandbox") | ||
| for file_id in file_ids: | ||
| path = save(file_id, model) | ||
| print(f" {file_id} -> {path} ({path.stat().st_size} bytes)") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(asyncio.run(run(sys.argv[1] if len(sys.argv) > 1 else "nebius:openai/gpt-oss-120b"))) |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: mozilla-ai/otari
Length of output: 20105
🏁 Script executed:
Repository: mozilla-ai/otari
Length of output: 38247
🤖 get_repo_knowledge executed:
get_repo_knowledge mozilla-ai/otari /tmp/coderabbit-repo-knowledge/mozilla-ai-otari-ed2c8c66/conventions /tmp/coderabbit-repo-knowledge/mozilla-ai-otari-ed2c8c66/learningsLength of output: 17552
🏁 Script executed:
Repository: mozilla-ai/otari
Length of output: 10877
Add an index for master-key file listings without a workspace filter.
list_filesalways filters byuser_id. A master-key request can omitworkspace_id, while the query still keyset-pages and orders by(created_at, id). The existing(user_id, workspace_id, created_at, id)index cannot provide that order whenworkspace_idis absent.Add
(user_id, created_at, id)in this migration and inFileObject.__table_args__, while retaining the workspace-scoped index.🤖 Prompt for AI Agents