I spent three weeks last month ripping out Anthropic-specific code from a review automation system we built for a logistics company. Three weeks. The work itself took maybe two days, the rest was tracking down subtle differences in how we'd structured prompts, handled errors, and shaped JSON responses. We'd convinced ourselves we were "flexible" because we'd abstracted the API calls. We weren't. We were just committed to a provider in ways that didn't show up until we needed to switch.
That experience is why this article hit different for me. Most "AI integration" posts feel like they're written by people who've never actually had to migrate a production system off one model provider to another. This one? It reads like someone's already made that mistake and learned the hard way.
The Real Decision Isn't Technical, It's Operational
Here's the thing that resonated most with me: the author frames this as an operations decision wearing a technical disguise. You're not really choosing between OpenAI and Anthropic based on model quality or API elegance. You're choosing which provider-specific behaviors you're willing to own for the next 18 months.
OpenAI compatibility wins for most teams because it's boring. There's middleware, examples, and a clear path to swap providers later. Your adapter layer stays thin. Anthropic's native API is genuinely good, but if you use it, you need to commit to understanding why their message contract matters for your use case, not just treating it as "another LLM API."
The trap I fell into was pretending I could abstract both equally. I couldn't. Every time Anthropic changed something about structured output or system prompt placement, I'd have to choose: hide the difference in my adapter (which makes portability theater) or accept that my application knows about provider-specific behavior.
Durability Matters More Than Integration Speed
What got my attention was the section on idempotency. In logistics code review, a timeout isn't just annoying, it means the review findings might get written twice, creating race conditions in the queue system. Most chatbot tutorials skip this entirely. They show you a happy path demo and assume your production infrastructure will just... work.
The author's approach is bulletproof: derive a stable review ID from repository path, commit SHA, policy version, and diff digest. Store that ID before calling the model. Make your final database write conditional on that same ID. You never end up with duplicate reviews even if the model API succeeds twice.
I'm doing exactly this now. The retry logic goes into the client (3 attempts, explicit budget), but the idempotency boundary wraps the entire review record. A 429 is retryable. A malformed finding isn't. A timeout is ambiguous, so the worker calls again, but the database write is protected by the review ID, not the API call.
// Stable review ID derived before any external calls
const reviewId = crypto
.createHash('sha256')
.update(`${repo}:${commitSha}:${policyVersion}:${diffDigest}`)
.digest('hex');
// Store the ID first, it anchors the entire operation
await db.reviews.insertIfNotExists({
id: reviewId,
status: 'pending',
createdAt: new Date(),
});
// Call the model with retry budget
let findings = null;
for (let attempt = 0; attempt < 3; attempt++) {
try {
findings = await modelProvider.review(diff, policy);
break;
} catch (err) {
if (attempt === 2) throw err;
await sleep(1000 * (attempt + 1));
}
}
// Write findings conditional on the same ID
await db.reviews.updateIfNotModified(reviewId, {
findings,
status: 'complete',
});
My Take: Test Your Switching Story Before You Need It
I agree with almost everything here, but I'd push one point further: don't just design for portability. Actually test it. Redact some real diffs, run them against your current provider, then swap to a competing provider and run them again. Compare findings. See where they diverge.
That experiment surfaced three behaviors in our system that we thought were generic but weren't: how we formatted code snippets in the prompt, how we weighted severity classifications, and how we handled edge cases in the policy engine. None of those are provider differences, they're application differences we'd baked in while thinking we were provider-agnostic.
I'd also push back slightly on the "OpenAI compatibility for everyone" stance. If you're already embedded in AWS or Google Cloud, using Bedrock or Vertex AI makes sense operationally, even if it adds a layer. Your governance, logging, and cost tracking are already there. Don't fight your platform for the sake of portability theater.
What Would You Do Differently?
I'm curious how you'd handle this if you were building something similar today. Would you start with the portable contract, or would you optimize for the provider you're actually committed to? And if you've already hit the "we need to switch providers" problem in production, I want to hear how painful it was.
The first time you realize your supposedly portable system depends on one provider's quirks is the same time you start thinking like an infrastructure person, not just an API consumer.
Source: This post was inspired by "In-App Chatbot Code Reviews, A Beginner's Portable API Contract" by Dev.to. Read the original article