I recently started building an independent API gateway for applications that use multiple AI providers.
At the beginning, I assumed the main challenge would be routing a request to the right model. The routing itself was relatively straightforward. The difficult part was making different providers behave consistently enough for an application to rely on them.
Here are the problems that took more time than expected:
A non-streaming response is usually easy to normalize. Streaming is different. Events can arrive in different shapes, usage information may only appear in the final event, and a tool call can be split across several chunks.
If the client assumes that every chunk contains ordinary text, the integration eventually breaks.
Tool calls are especially difficult because the function name, arguments, and status may not arrive together. The application needs to accumulate partial data and only try to parse the arguments when the stream is complete enough.
Malformed or incomplete JSON also needs to produce a useful error instead of silently failing.
Some APIs represent content as a string. Others return an array of content blocks containing text, images, tool results, or other data.
Treating every response as a string works for simple demos, but not for a real application that supports multiple message formats.
Different providers use different status codes, error fields, and messages. I had to normalize the errors while preserving enough information for debugging.
A generic “request failed” message is not useful. At the same time, returning raw provider responses can expose implementation details that the client should not depend on.
Retries look simple until the request has side effects or consumes a large amount of tokens. A retry after a timeout does not necessarily mean the provider never processed the original request.
This makes idempotency, request tracking, and clear retry policies important, especially for agent workflows.
Model names alone are not enough. Applications also need to know whether a model supports streaming, tools, vision, structured output, long context, or a particular message format.
I started maintaining a small capability matrix instead of assuming that all models behind a compatible endpoint behave the same way.
My current testing process includes:
The next part I’m working on is better observability: making failures easier to diagnose while avoiding the collection of sensitive prompt or API-key data.
I’m curious how other indie hackers handle this. Do you keep provider-specific logic inside one adapter, or do you expose the differences to the application layer?
For context, I’m building luoluoAI, an independent gateway with OpenAI-compatible and Claude Messages-compatible interfaces. I’m sharing the engineering problems here because the compatibility work has been more interesting than the initial routing layer.
The strongest part is the compatibility problem, not the routing. Normalizing streaming, tool calls, capabilities, errors, and retries across providers is what makes an AI gateway genuinely valuable once applications depend on it.
yes, compatibility of gateway is solid problem what I'm facing, it dose require unite various providers
That’s the part that makes the gateway more than just another routing layer. I’d be interested in continuing the conversation privately. What’s the best email to reach you at?
welcome to detail discussion. siofeng@qq.com
Thanks! I’ve just sent it over.
Looking forward to hearing your thoughts whenever you have a chance.
The provider timeout/retry problem gets so much worse once you add streaming responses into the mix, since a dropped connection mid-stream leaves you guessing how much the user was actually billed for. What's worked for me is treating every provider call as an idempotent operation with its own request ID up front, so retries just replay against that ID instead of re-triggering side effects. I'm curious whether you're also normalizing streaming behavior across providers, since that's usually messier than the sync case.
The line I keep rereading is that a retry after a timeout does not mean the provider never processed the original request. That is the part people discover in billing, not in tests.
When scope keeps widening like this, the move that saved me was carving a weekend slice: pick one provider, one streaming path, one tool call, and make that single route trustworthy end to end before the capability matrix grows another column. A narrow slice that survives real traffic teaches you more than five half normalised ones.
Full version of that framing here: https://durablefoundations.gumroad.com/l/pyramid-reality-check
Which provider would you pick if you could only guarantee one?
Kael Voss / DurableFoundations