
How I Migrated a Lambda AI App to Bedrock’s OpenAI-Compatible APIs Without Rewriting Everything
A practical migration story about moving a Lambda AI app from a custom OpenAI-style integration to Amazon Bedrock without rewriting the whole codebase

A practical migration story about moving a Lambda AI app from a custom OpenAI-style integration to Amazon Bedrock without rewriting the whole codebase
Draft note: This article is drafted for review only. It will stay out of the public blog index until I publish it.
I wanted to move one of my Lambda AI apps off the old setup and onto Amazon Bedrock without turning it into a rewrite project.
That mattered because the app already worked. It accepted a request, built a prompt, called a model, handled retries, logged traces, and returned a response. The architecture was fine. The problem was the underlying model provider path had started to feel like something I would rather replace before it turned into a dependency headache.
Bedrock’s OpenAI-compatible APIs looked like the cleanest exit. The pitch was simple: keep the shape of the client calls familiar, switch the backend, and avoid rewriting the whole application around a new inference layer.
That is the kind of migration I like. Not dramatic. Just boring in the best possible way.
The app was a pretty standard Lambda-backed API. A request came in through API Gateway, Lambda normalized the payload, then the handler called a model provider and returned the result.
The code already had a few production-minded pieces around it:
That structure mattered because it meant the migration target was not the whole app. It was mostly the model client and the surrounding config.
Here is the important part: I was not trying to change the product behavior. I was trying to change the model integration while keeping the request flow, error handling, and observability intact.
I was not looking for novelty. I was looking for less friction.
Bedrock gave me a few things I wanted right away:
That last point was the big one. If a migration forces me to rewrite every prompt wrapper, every request mapper, and every retry branch, the cost is usually not worth it unless there is a very strong reason.
In this case, the compatibility layer meant I could keep the application structure and mostly swap the backend implementation.
These parts stayed the same, which is why the migration was manageable:
That is the real reason the migration stayed small. I treated the model client as an adapter, not as the center of the application.
The changes were smaller than I expected.
At a high level, I changed four things:
The client code became a thinner wrapper around the Bedrock-compatible request shape. I did not want provider-specific logic leaking into the rest of the app, so I kept all of that behind one interface.
A simplified version looked like this:
type LlmRequest = {
prompt: string;
model?: string;
temperature?: number;
};
type LlmResult = {
text: string;
model: string;
promptTokens?: number;
completionTokens?: number;
};
export async function generateText(input: LlmRequest): Promise<LlmResult> {
const client = getBedrockClient();
const response = await client.send({
prompt: input.prompt,
model: input.model ?? 'claude-sonnet-4',
temperature: input.temperature ?? 0.2,
});
return {
text: response.text,
model: response.model,
promptTokens: response.usage?.inputTokens,
completionTokens: response.usage?.outputTokens,
};
}
The important thing is not the exact SDK syntax. It is the shape of the boundary. The rest of the app only cared that it could ask for text and get text back.
That kept the migration honest.
Every migration has a few annoying surprises.
The first one was auth. Even when the API shape looks familiar, the authentication story is different. Instead of a provider API key flowing through the app, I had to make sure the Lambda execution role had the right Bedrock permissions and that the environment was using the right AWS identity.
The second surprise was request assumptions. Some of my tests had quietly encoded the behavior of the old provider. They were checking response formatting, token fields, or a specific error message that no longer matched Bedrock’s response shape exactly.
The third surprise was prompt framing. The prompt itself did not need a rewrite, but I did need to verify that the model behaved the same way with the new backend. Small differences in output style, truncation, or refusal behavior can show up even when the request format looks equivalent.
So I did what I usually do when a migration looks simple but not trivial: I added a few focused checks instead of trying to solve everything with one giant integration test.
If you are planning a similar migration, these are the gotchas I would watch first:
That last one was the most annoying. The more exact a test is about provider-specific wording, the more likely it is to become a migration tax later.
I did not try to preserve the old tests exactly.
Instead, I rewrote the tests to validate what the app actually needed to guarantee:
That made the tests less brittle and more useful for the new provider.
I also added one or two path-specific tests for the new Bedrock branch so I could prove the migration did not just compile. It had to run through the real call path at least once in a safe environment.
The best part of a migration like this is not the shiny new provider. It is the reduction in mental overhead.
After the switch, the app felt simpler to operate because the model access now lived more naturally inside the AWS stack. I had fewer places to think about credentials, fewer pieces of provider-specific glue, and a cleaner story for who can call what.
That did not magically solve every problem. It did not make prompts better by itself. It did not remove the need for observability. It did not eliminate model differences.
But it did make the app easier to own.
That matters. A lot of engineering work is not about finding the perfect architecture. It is about removing avoidable complexity from the parts you have to keep living with.
My rule is simple: if the provider change can be contained behind one adapter, I will usually do it. If it forces the rest of the application to become provider-aware, I slow down and ask whether the move is still worth it.
That is the difference between a migration and a rewrite.
In this case, Bedrock’s compatibility layer gave me enough surface-area overlap to keep the app intact. I changed the client, tightened the tests, checked the auth model, and kept the rest of the system steady.
That is exactly how I like migrations to go: small enough to verify, boring enough to trust, and useful enough to keep.
If you are considering a similar move, my advice is to start by isolating the model client behind a clean interface. If you do that early, the rest of the migration gets a lot easier.
And if you can avoid rewriting everything while still improving the stack, that is usually the best outcome.
I email when a new post goes up. One send a week, and only if there's something new.
Want this applied on your account? Start with MLOps.
Related reading
Kept below the post instead of in a sidebar, with a slow continuous motion for a cleaner editorial feel.
On this post
Comments
A reply stays under the note it answers.
No comments yet.
If you have a note on How I Migrated a Lambda AI App to Bedrock’s OpenAI-Compatible APIs Without Rewriting Everything, sign in and leave it.