Ploy Switches Production AI from Opus 4.8 to GPT-5.6 Sol: Time Cut by More Than Half, Cost Down 27%, No Quality Loss

Swapping models isn't just swapping an API: four invisible traps — eval framework, tool parameters, caching, reasoning records — unpacked and fixed one by one
By Ploy Blog07-13 · 9 min read⚑ Company PR · read with that in mind
🎧 Listen to the podcast0:00 / --:--
0:00 / --:--
Deep Dive · XiaoHu Explains
TL;DR
  • Ploy switched the default model behind its website-building AI agent from Claude Opus 4.8 to GPT-5.6 Sol, which launched that same day
  • Its own benchmarks show: for the same kind of website-building task, time dropped from 8:00 to 3:42, cost fell from $3.06 to $2.22, and the visual score rose from 0.936 to 0.970
  • During migration, they found GPT-5.6 invents default values for tool-call parameters it doesn't need (like offset: 0), which at one point silently caused 52% to 64% of file reads to return empty content
  • OpenAI's prompt caching mechanism is completely different from Anthropic's; after redesigning the cache layering with a workspace-level cache key, the first-call cache hit rate went from 0% to 83.7%
  • The team also found their own eval framework had a hidden bias toward the old model it was used to, so the framework itself had to be fixed before its results could be trusted
⚑Stance note: This piece is Ploy's official engineering blog — a vendor's own account. The time, cost, visual scores, cache hit rates, parameter-inclusion rates, and other figures cited here all come from Ploy's own eval framework and production traffic, and the benchmark standards are Ploy's own. What follows just relays the facts and practices it recorded.
1Background · Model Switch

Ploy Switched Its Default Model to GPT-5.6 Sol

On July 9, 2026, AI website-building product Ploy switched its agent's default model from Claude Opus 4.8 to GPT-5.6 Sol, which OpenAI released that same day.

Switching models turned out to be far more complicated than swapping an API endpoint. Ploy's agent has to plan pages, read the codebase, write components, generate images, screenshot its own output, and then judge whether it's actually done. Moving this whole pipeline from Claude to GPT-5.6, the team discovered — one failed eval after another — that what gets treated as "the model" is actually each provider's own set of behavioral habits, and the whole stack had quietly grown into a shape built around the old model.

📊
Same production-grade agent, same Vercel AI SDK — only the default model changed. In Ploy's own benchmarks, for the same kind of website-building task: time went from 8:00 down to 3:42 (about 2.2x faster), cost went from $3.06 down to $2.22 (27% cheaper), and the visual score rose from 0.936 to 0.970. For the past four months, Opus had held the default slot (first 4.7, then 4.8), and none of the models the team tested had beaten it — GPT-5.6 is the first one that did.
Colorful inflatable shapes connected by translucent tubes, a metaphor for an AI agent migrating between two model systems
Ploy official blog image: several colorful inflatable shapes connected by a single translucent tube, a metaphor for a production-grade agent migrating between two model systems. Source: Ploy Blog

Below is a sample from its brand-page-rebuild eval suite, where the agent has to redo a brand homepage to match a reference design:

Average per completed buildClaude Opus 4.8 (n=11)GPT-5.6 (n=10)
Cost$3.06$2.22
Time8:003:42
Input tokens2.60M1.70M
Output tokens33.0K17.1K
Visual score0.9360.970

GPT-5.6's code is leaner. In one paired sample, Opus generated a globals.css with 17,957 characters and 174 CSS variables (large chunks of the color scale went unused), while GPT-5.6 used only 2,508 characters and 45 variables to render a comparable — sometimes better — page.

The paired sample described above, as rendered by each model: Opus 4.8 on the left, GPT-5.6 Sol on the right. GPT-5.6 delivered a comparably finished page with roughly one-seventh the CSS. Only the top of each page is shown; click an image for the full-length screenshot. Source: Ploy Blog
Related coverage on this site
OpenAI Officially Launches GPT-5.6: Overall Intelligence Score Closing In on Claude Fable 5, Cost Cut in Half, ChatGPT and Codex Merged
That piece covers what GPT-5.6 itself shipped; this one is a production user's real-world migration record from the day it launched.
2Is the Eval Fair

Check Whether the Eval Framework Is Fair Before Judging Which Model Is Stronger

Before comparing two models' scores head-on, you have to ask one thing first: is the eval framework itself fair? Ploy ran the same eval suite across both model families, and the most surprising finding was that the framework had already been tuned around the old model — without the team realizing it.

On the first cross-model run, the bias showed up in a few specific places:

  • The tool-call budget was set around Opus's sequential calling style. GPT-5.6 fans out a bunch of calls in parallel, blowing through the budget even on tasks it actually got right.
  • The eval runner didn't support batch file reads — a move Opus rarely used but GPT-5.6 used constantly.
  • One dataset was missing a minScore threshold and silently inherited the default value of 1.0. So a GPT-5.6 hero section that scored 0.98 got marked "failed," and an Opus task that passed every individual check also counted as a "failure." Both were defensible design directions — what tripped them up was an invisible threshold.

Tracing this round's raw failures back, about a third of them turned out to be caused by the eval framework's assumptions, not actual model behavior — and the two models didn't trip over them equally.

Opus's gradient hero scores 1.0 and passes; GPT-5.6's color-block approach scores 0.98 but fails due to a default threshold
Left: Opus's gradient hero scores a perfect 1.0 and passes. Right: GPT-5.6's color-block approach scores 0.98, but gets marked as failed simply because it hit an unwritten hidden threshold that defaulted to 1.0. Source: Ploy Blog

The takeaway is direct: when evaluating a challenger model, read through the traces line by line before trusting the pass rate — otherwise you're grading the new model on "how much it resembles the old one."

3Design Capability Traits

Design Style: Clean and Attractive, but Prone to Sameness

Ploy's overall read on GPT-5.6's design ability: it's very good at clean, modern layouts with tidy grids, but left unguided, it converges toward that look every time. Under the old eval framework built for Opus 4.8, GPT-5.6 tended to ignore existing design systems and produce pages that were crisp and restrained, but unmistakably generic at a glance.

Opus replicates Clay's brand system; GPT-5.6 produces a clean but generic page
Left: Opus replicated Clay's visual brand system. Right: under an eval framework designed for the old model, GPT-5.6 produced a clean but generic page. Source: Ploy Blog

How to guide it toward world-class brand fidelity is a topic for another post. This is just noting a real capability trait — not really a pitfall.

4Fabricated Tool Parameters

The Model Invents a Fake Value for Parameters It Doesn't Need

Ploy's code tool has 25 top-level parameters, of which only action is required — the rest are optional. Claude sends only the two or three it actually needs and omits the rest. GPT-5.6 fills in all 25 every single time, inventing a plausible-looking value for whatever it doesn't need: offset gets 0, timeout gets 120000, siteId gets a UUID that's all zeros.

Silent Failure

Here's the crux: at the interface level, there's no way to tell an invented value apart from a real one. offset: 0 looks like a perfectly normal parameter, so Ploy's file-read implementation took it at face value — which meant 52% to 64% of GPT-5.6's file reads silently came back empty. But the tool returns success: true either way, so the model has no idea it just read an empty file. It just quietly keeps doing worse work, piling on more and more calls.

Over three days of production traffic, the share of code(read) calls carrying all 25 parameters was different enough that it barely looks like the same tool:

GPT-5.6100%
Opus 4.80.1%
Sonnet 50%

In absolute numbers: all 6,635 of GPT-5.6's calls came fully loaded; only 4 of Opus 4.8's 2,898 calls did; and none of Sonnet 5's 1,933 calls did.

Prompting can't fix this. Ploy tried every way it could think of to "talk it out of it," and all of them failed outright:

✗ Writing "omit parameters you don't need" in the tool description

Still filled in all 25.

✗ Marking every property individually as "OPTIONAL, omit if unused"

Still filled in all 25.

✗ Turning on OpenAI's official strict mode

Same behavior either way (per Ploy's own testing), and using it also requires stripping pattern, format, and array bounds validation out of every schema — an even bigger cost.

This is a behavior baked into how the model generates function calls at a fundamental level — instructions can't talk it out of it, so the only option is to design around it.

The fix that actually worked was a schema transformation at the provider boundary: only for OpenAI-family models, rewrite every optional property as "required but nullable" (using anyOf: [T, null]), so the model has a proper way to explicitly say "I'm not using this." Then, at the one checkpoint every tool call passes through, strip those nulls back out — the tool side doesn't need a single line changed. The net effect: the model sees a schema that lets it be honest, and the tool still gets the same input it always got.

Schema transformation: turning optional tool parameters into "required but nullable"
// Before: 25 keys, every one carrying an invented value
{ "action": "read", "file_paths": [...], "offset": 0, "timeout": 120000, ... }

// After: 25 keys, 4 real values, 21 explicit nulls (stripped before the tool runs)
{ "action": "read", "file_paths": [...], "offset": null, "timeout": null, ... }
The model sees a schema that allows null
→
The model uses null to explicitly say "not using this"
→
Before reaching the tool, the one checkpoint strips the nulls
→
The tool receives the same input as before
Before
52% to 64% of file reads came back empty. The model had no idea, so it just read again and again, piling up more calls.
After
Empty file reads dropped to 0%. The same work now uses about 30% fewer tool calls, since it's no longer re-reading empty files over and over.
5Two Different Caching Designs

The Two Providers' Prompt Caching Are Fundamentally Different Designs

On the surface both providers call it prompt caching, but underneath that one word "caching" sit two completely different architectures. This is the single thing in the migration that needed the most care: before it was set up correctly, GPT-5.6 looked about 50% more expensive than Opus — and that extra cost came entirely from Ploy's own cache configuration, nothing to do with model pricing.

Ploy's prompt starts with a roughly 29K-token static prefix (tool schemas plus the core system prompt) that's identical in every single conversation. How the two providers cache that prefix is worlds apart:

Anthropic (Claude)
Marks breakpoints with cache_control. That 29K static prefix is shared org-wide — any conversation, any workspace draws from the same copy, and there's no need to worry about how many requests per minute it can handle. The hit rate stays steady at 92% to 96%; caching is basically invisible.
OpenAI (GPT-5.6)
Changed implicit caching from "partial prefix matching" to building a "whole-prefix" entry keyed only on the latest message. A new conversation gets 0% hits even with the identical 29K prefix, and every cache miss costs an extra 1.25x cache-write surcharge on top.

OpenAI's official mechanism is explicit: mark with prompt_cache_breakpoint, paired with a required prompt_cache_key. The key is exactly where the design really forks, because it's part of the cache's identity. Same prompt, different key, and the hit rate is 0. Each key maps to one cache node, and that node can handle roughly only 15 requests per minute — go over that, and OpenAI routes the overflow to other, cold cache nodes.

Here's an analogy

prompt_cache_key is like a reserved-seat tag at a library: people holding the same tag get seated in the same section and can pick up right where they left off next time. Same key means sharing the same batch of cache entries; change the key, and it's like switching sections — none of the previously stored cache carries over.

So "whether to enable caching" turns into a real design decision: what level do you bind this key to? The three approaches each play out differently:

① Assign the key per conversation

A new conversation never hits the shared prefix — first-call hit rate is 0% (Ploy hit this pitfall in practice, and it was expensive).

② One global key for everything

All requests hash to the same cache node, and production traffic instantly blows through the 15-requests-per-minute budget; requests get bounced to cold, uncached nodes, and you're back to misses.

③ Assign the key per workspace (the sweet spot)

All conversations within the same customer workspace share cache entries, and the traffic pressure on any single key stays low.

What Ploy ultimately shipped was a workspace-level key: requests get hashed to a throughput-limited cache node, which effectively gives every customer workspace its own dedicated lane:

Workspace Aws:A key Workspace Bws:B key Workspace Cws:C key hashprefix + key Cache node≤15 req/min Cache node≤15 req/min Cache node≤15 req/min
Each customer workspace gets its own key — effectively its own dedicated lane, with requests going to their own cache node. Switch to one global key, and the three lanes get squeezed into one; once that node passes 15 requests per minute, requests get bounced to cold, uncached nodes.

The key alone wasn't enough — Ploy also split the system prompt into three tiers of breakpoints, each handling one job, aligned with the structure it uses for Anthropic:

A
[ tool schemas + static prefix ]Every conversation hits this one, keeping a session's first call cheap.
B
[ tool schemas + static prefix + workspace context ]When workspace memory changes, B misses but A still hits — only the context-sized B needs to be rewritten, without re-billing the full 29K prefix.
C
[ … + turn 1 + … + latest message ]This session's chain; since Ploy's prompt only ever appends and never edits earlier content, OpenAI's implicit whole-prefix chain stays valid for the entire session.
0% → 83.7%
Hit rate on a new session's first call, after rebuilding the cache
−28%
Drop in total input tokens from cache misses
92%~96%
For comparison: Claude-side org-wide hit rate on the same 29K prefix
1.25×
Extra cache-write surcharge per prompt when GPT-5.6 misses the cache

After the fix, GPT-5.6's per-suite eval cost dropped below Opus's. Every dollar of the price gap the team had been staring at turned out to be a misconfigured cache — nothing to do with model pricing.

There's also one structural limitation that can't be worked around: on OpenAI, sharing that static prefix across workspaces just isn't structurally possible. Anthropic can share it because its cache is partitioned by organization, not by key. On GPT-5.6, once a given workspace's cache sits idle and expires, the next call has to pay for a cold 29K write again — about $0.18. That's a real cost, but it's bounded and predictable.

6Reasoning Records

The Previous Turn's Reasoning Record Can Suddenly Go Missing — One Flag Fixes It

The last pitfall is the shortest one, but it can genuinely break a conversation. By default, GPT-5.6's Responses API stores the previous turn's reasoning as a "server-side item reference": the server keeps a numbered record, and the next turn just references that number. Ploy's conversations started intermittently throwing mid-conversation errors: Item 'rs_...' not found.

Here's an analogy

A server-side pointer is like handing you a pickup receipt instead of the item itself. Once the receipt expires, you can't claim your goods anymore. store: false is the equivalent of handing the goods straight to you — no more worrying about an expired receipt.

Default: Server-Side Pointer
The previous turn's reasoning gets stored as a server-side ID (rs_...), and the next turn just queries that ID. Once the ID expires, it throws Item 'rs_...' not found.
store: false: Self-Contained
The SDK instead requests "encrypted reasoning content," turning the reasoning replay into a self-contained blob of data that carries everything it needs, no longer dependent on server-side state.
The fix for cross-turn reasoning-replay errors
store: false — tells the SDK to request "encrypted reasoning content" instead, turning reasoning replay into a self-contained data blob
rather than a pointer reference to server-side state; otherwise it can throw Item 'rs_...' not found mid-conversation

One more thing cost Ploy an extra afternoon of debugging: once the round-trip carries server-side reasoning state, the effective upstream prompt can still change — even if the bytes you're sending strictly only append, with not a single one touched.

7Scorecard and Checklist

After the Migration: The Final Scorecard and a Checklist for Similar Teams

With all four pitfalls patched one by one, the numbers promised up front held up: about 2.2x faster, 27% cheaper, visual score flat-to-slightly-up; after reconfiguring the cache, first-hit rate went from 0% to 83.7%, missed-cache input tokens dropped 28%, and per-suite cost fell below Opus; after the schema fix, empty file reads hit zero and tool calls dropped by about 30%.

These four steps have a required order — fix the framework first, or you can't trust any of the numbers that follow:

Fix the Eval Framework
The tool-call budget was set around the old model's sequential-calling habits, and hidden thresholds like minScore defaulting to 1.0 were lurking underneath. About a third of the first round's failures turned out to be the framework's fault, not the model's.
Fix the Tool Schema
GPT-5.6 fills every optional parameter and invents default values, silently returning empty file reads. Change optional parameters to anyOf: [T, null] at the provider boundary, then strip the nulls before execution.
Rebuild the Cache
OpenAI needs an explicit prompt_cache_breakpoint plus a prompt_cache_key, with the key bound per workspace, and the system prompt split into three tiers, A / B / C. First-hit rate went from 0% to 83.7%.
Make Reasoning Replay Self-Contained
Add store: false, replacing the server-side pointer with an encrypted, self-contained data blob, fixing the Item 'rs_...' not found error.
✅ Migrating from Claude to GPT-5.6: All Four Steps Are Required
If you're comparing the cost of two models and one of them still has a cold cache, you're comparing your own configuration, not the models.Ploy's official engineering blog
A four-step checklist for migrating from Claude to GPT-5.6, plus the schema-transform code, the cache-key decision list, and the store:false fix — copy it and get started.
Source: Ploy's official engineering blog, "Migrating a production AI agent to GPT-5.6" (ploy.ai/blog, July 9, 2026). This piece is XiaoHu Explains' Chinese-language visual interpretation; the migration gains, hit rates, parameter-inclusion rates, and other data cited are all Ploy's own benchmark figures, and the charts here were redrawn by this site based on the original.
Source
Ploy BlogPloy Blog·Read the source
↑ End of article
✓ This article is free to read in full

Members can copy or download the full Markdown, and let their AI subscribe and ask.

Read the original ↗
📮 New explainers, straight to your inbox

Found this useful? Leave your email — every new explainer, the moment it's out. Unsubscribe anytime.