I spent a weekend trying to move a custom GPT onto local weights and got the model part done in about an hour. The rest of the weekend went to one feature I had never once thought about: Code Interpreter.
My assumption going in was that writing Python was the hard part, so a model that writes decent Python covers it. That assumption is wrong in a specific way, and it is worth naming because the same mistake shows up in every "local model vs ChatGPT" comparison I have read.
Writing Python is the easy half. The hard half is running it.
What you are actually replacing
When a user uploads a CSV to a custom GPT and asks it to plot something, here is the shape of what happens on OpenAI's side:
- a container spins up
- the file lands in it
- generated code executes against that file
- the container has no network
- it dies on a timeout
- nothing it did can touch anything else
Every one of those is a decision someone made and then operated. None of them is a prompt. You do not get any of them by downloading weights, no matter how good the weights are at writing pandas.
So when you move off a custom GPT, this is not a capability that ports and it is not a capability that disappears. It is a capability that becomes yours to host. That distinction turns out to matter a lot for planning, because the three categories behave completely differently:
| Category | Example | What it costs you |
|---|---|---|
| Ports as-is | Your Actions endpoint | Nothing, it was already your HTTP API |
| You rebuild | Code Interpreter | Evenings, plus something you now operate forever |
| Does not port | Image generation | A second model, or a no |
I had been treating all three as one bucket labelled "stuff I lose", which made the whole decision look scarier than it was, while simultaneously filing "I'll just add a sandbox" under Saturday afternoon, which made the actual work look easier than it was.
The naive version and why it is not fine
The first thing anyone writes is this:
exec(generated_code, {"__builtins__": {}})
This is not a sandbox. Stripping builtins is a speed bump for anything that can reach a class hierarchy, and a generated script does not have to be malicious to hurt you here. A model that writes an accidental infinite loop, or an accidental while True that appends to a list, does the same damage as one that was trying to.
The actual requirements, once I wrote them down honestly:
- Filesystem isolation, because the user's uploaded file should be the only file the code can see
- No network, because a model that decides to
pip installsomething is a model that just gave a package registry your execution context - A wall clock timeout that kills the process, not one that asks it nicely
- A memory ceiling, for the same reason as the timeout
- Some way to get the plot back out, which sounds trivial until the container is correctly isolated and now you need a channel
All of this is available. gVisor, Firecracker, a locked-down Docker container with --network none and a cgroup limit, or one of the hosted sandbox APIs if you would rather rent it than run it. None of it is exotic. What I keep coming back to is that on the ChatGPT side, items 1 through 5 were a checkbox in a form.
What it changed about my decision
I ended up not moving the assistant that used Code Interpreter, and moving two others that did not. That sounds like a compromise but it was actually the useful outcome, because it forced the question to be asked per capability instead of per assistant.
The move is worth it when the reason is that data cannot leave your machines, or when you want the model to still behave the same in five years regardless of what a pricing page says by then. It is not worth it when your real bill is engineering time. Two evenings rebuilding retrieval costs more than a year of Plus, and I say that as someone who spent the two evenings.
If you want to run the same audit against your own build rather than mine, the Muse Glimmer vs GPTs capability checker does exactly this: you tick which features your GPT actually uses, and it splits them into ports, rebuild, and blocked, with the reasoning shown for each. It also asks how much VRAM you have before it tells you anything encouraging, which I appreciated after being told by three separate blog posts that the model needs anywhere from 17 GB to 64 GB.
If I were starting the weekend again, I would skip the benchmarking entirely on day one and just list what my assistant uses, marking each feature as code or as operations. The model comparison I spent most of Saturday on turned out not to be the comparison that decided anything.
Top comments (0)