Prompt
How do I standardize request and response formats across OpenAI, Anthropic, and Gemini?
Latest observation
A good way to standardize across OpenAI, Anthropic, and Gemini is to define your own internal canonical request/response schema, then build thin provider adapters that translate to/from each vendor’s API.
1) Pick a canonical schema
Keep it simple and model-agnostic:
Canonical request
{
"model": "optional-string",
"messages": [
{
"role": "system|user|assistant|tool",
"content": [
{
"type": "text",
"text": "Hello"
}
]
}
],
"tools": [
{
"name": "get_weather",
"description": "Fetch weather",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}
}
],
"tool_choice": "auto|none|specific",
"temperature": 0.7,
"max_tokens": 1000,
"stream": false,
"metadata": {}
}
Canonical response
{
"id": "req_123",
"provider": "openai|anthropic|gemini",
"model": "string",
"output": [
{
"type": "text",
"text": "Final answer"
}
],
"tool_calls": [
{
"name": "get_weather",
"arguments": { "city": "Paris" }
}
],
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0
},
"finish_reason": "stop|length|tool_calls|content_filter|error",
"raw": {}
}
2) Normalize the concepts, not the exact wire format
The three APIs differ in structure:
OpenAI
- Chat/Responses-style messages
- Tool calls are explicit
- Supports structured output / JSON schema in some endpoints
Anthropic
- Uses
messagesplus separatesystem - Tool use is represented differently
- Content blocks are common (
text,tool_use, etc.)
Gemini
- Uses
contents,parts, andsystemInstruction - Tool calling is represented via function declarations / function calls
- Response structure is different again
Instead of trying to make them identical, map them into your canonical layer.
3) Use adapters for each provider
Adapter responsibilities
Each provider adapter should:
- Convert canonical request → provider request
- Call the API
- Convert provider response → canonical response
- Preserve raw response for debugging
Example pseudo-interface
interface LLMProviderAdapter {
buildRequest(canonical: CanonicalRequest): unknown;
parseResponse(providerResponse: unknown): CanonicalResponse;
}
4) Standardize message format internally
A practical internal message shape is:
type CanonicalMessage = {
role: "system" | "user" | "assistant" | "tool";
content: Array<
| { type: "text"; text: string }
| { type: "image"; url?: string; base64?: string; mimeType?: string }
| { type: "tool_result"; toolCallId?: string; text: string }
>;
};
Why this helps:
- Supports multimodal inputs
- Makes tool results explicit
- Avoids provider-specific assumptions
5) Standardize tools/function calling
Use one internal tool schema, preferably JSON Schema:
{
"name": "search_docs",
"description": "Search internal docs",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"],
"additionalProperties": false
}
}
Then translate:
- OpenAI → function/tool schema
- Anthropic → tool definitions
- Gemini → function declarations
Also standardize tool call output internally as:
{
"name": "search_docs",
"arguments": { "query": "pricing" }
}
6) Standardize streaming if you need it
Streaming is where providers differ the most.
Recommended approach:
- Internally define your own event types:
message.deltatool_call.deltamessage დასრულվածerror
- Convert each provider’s streaming events into those
Example canonical stream event:
{
"type": "message.delta",
"text": "Hello"
}
This makes UI and orchestration code provider-agnostic.
7) Keep metadata and usage consistent
Define internal usage and metadata fields:
{
"usage": {
"input_tokens": 123,
"output_tokens": 456,
"total_tokens": 579
},
"metadata": {
"request_id": "abc",
"latency_ms": 1200,
"cache_hit": false
}
}
Each provider may report different token accounting, so store both:
- normalized usage
- raw provider usage
8) Recommended architecture
Layer 1: App code
Uses only canonical request/response objects.
Layer 2: Provider adapter
One adapter each for OpenAI, Anthropic, Gemini.
Layer 3: Provider SDK/API
Actual vendor calls.
This keeps vendor lock-in low and makes testing easier.
9) A minimal example flow
- Your app creates a canonical request.
- Adapter converts it to OpenAI/Anthropic/Gemini format.
- Provider returns a response.
- Adapter parses response into canonical response.
- App processes output the same way regardless of provider.
10) Practical tips
- Store raw payloads for debugging and audits.
- Use JSON Schema for tool inputs and structured outputs.
- Make your canonical schema strict and validate it.
- Write contract tests for each adapter.
- Expect edge cases:
- multiple assistant parts
- partial tool calls in streaming
- different finish reasons
- provider-specific safety blocks
11) If you want the simplest possible standard
If you don’t need full multimodal/tool complexity, standardize on:
{
"input": [
{ "role": "system", "text": "..." },
{ "role": "user", "text": "..." }
],
"output": {
"text": "...",
"tool_calls": []
}
}
That’s enough for many apps, and you can expand later.
If you want, I can also provide:
- a TypeScript canonical schema + adapters, or
- a Python implementation for OpenAI/Anthropic/Gemini normalization.