A bidirectional translation layer between the OpenAI Chat Completions API and Google's Gemini API. Store all LLM conversations in one OpenAI-compliant format, regardless of the underlying provider.
- Request conversion -
ChatCompletionCreateRequest→ GeminiGenerateContentRequestcomponents - Response conversion - Gemini
GenerateContentResponse→ OpenAIChatCompletion - Streaming - Gemini streaming chunks → OpenAI
ChatStreamEvents - Tool calling - Bidirectional tool definitions, tool calls, and tool results with automatic JSON Schema sanitization (
anyOf/oneOf/allOfflattening) - Thought signatures - Preserves Gemini 3+ cryptographic thought signatures across multi-turn tool-calling sequences
- Thinking/reasoning - Maps OpenAI
reasoning_effort↔ GeminithinkingConfig.thinkingLeveland surfaces thinking tokens asreasoningContent - Cross-provider interop - Maintain a single conversation history and freely switch between OpenAI and Gemini models mid-conversation
dependencies:
open_ai_gemini: ^0.1.0import 'package:open_ai_gemini/open_ai_gemini.dart';
import 'package:openai_dart/openai_dart.dart' as oai;
import 'package:googleai_dart/googleai_dart.dart' as gai;
// Build your request in OpenAI format.
final request = oai.ChatCompletionCreateRequest(
model: 'gemini-3-flash-preview',
messages: [
oai.ChatMessage.system('You are a helpful assistant.'),
oai.ChatMessage.user('What is the capital of France?'),
],
tools: [
oai.Tool.function(
name: 'lookup_capital',
description: 'Look up the capital of a country.',
parameters: {
'type': 'object',
'properties': {
'country': {'type': 'string'},
},
'required': ['country'],
},
),
],
);
// Convert to Gemini format.
final gemini = ChatCompletionRequestConverter.convert(request);
// Send to Gemini API.
final response = await geminiClient.models.generateContent(
model: 'gemini-3-flash-preview',
request: gai.GenerateContentRequest(
contents: gemini.contents,
systemInstruction: gemini.systemInstruction,
tools: gemini.tools,
toolConfig: gemini.toolConfig?.toJson(),
generationConfig: gemini.generationConfig,
),
);final result = ChatCompletionResponseConverter.convert(
response,
model: 'gemini-3-flash-preview',
);
// Standard OpenAI ChatCompletion object.
final completion = result.completion;
print(completion.choices.first.message.content);
// Preserve thought signatures for the next turn (Gemini 3+).
final thoughtSignatures = result.thoughtSignatures;Gemini 3 attaches cryptographic thought signatures to function calls that must be returned in subsequent turns. This package handles this automatically:
// Accumulate signatures across turns.
var signatures = <String, String>{};
// Turn 1: Get response with tool calls.
final result1 = ChatCompletionResponseConverter.convert(response1, model: model);
signatures = {...signatures, ...result1.thoughtSignatures};
// Store the assistant message in your OpenAI-format history.
history.add(result1.completion.choices.first.message);
history.add(oai.ChatMessage.tool(toolCallId: '...', content: '...'));
// Turn 2: Pass signatures back when converting the next request.
final gemini2 = ChatCompletionRequestConverter.convert(
nextRequest,
thoughtSignatures: signatures, // Re-injected into FunctionCallParts
);// Using the transformer class.
final transformer = GeminiStreamEventTransformer(model: 'gemini-3-flash-preview');
final openAIStream = geminiStream.transform(transformer);
await for (final event in openAIStream) {
final delta = event.choices?.first.delta;
if (delta?.content != null) print(delta!.content);
}
// Or using the convenience function (also captures thought signatures).
final result = convertGeminiStream(geminiStream, model: 'gemini-3-flash-preview');
await for (final event in result.events) { /* ... */ }
final signatures = await result.thoughtSignatures;The primary use case: store all conversations in OpenAI format and freely switch providers.
final history = <oai.ChatMessage>[
oai.ChatMessage.system('You are a geography expert.'),
];
// Turn 1: Use GPT.
history.add(oai.ChatMessage.user('What continent is France in?'));
final gptResponse = await openAIClient.chat.completions.create(
oai.ChatCompletionCreateRequest(model: 'gpt-4.1-nano', messages: history),
);
history.add(gptResponse.choices.first.message);
// Turn 2: Switch to Gemini seamlessly.
history.add(oai.ChatMessage.user('And Japan?'));
final geminiReq = ChatCompletionRequestConverter.convert(
oai.ChatCompletionCreateRequest(model: 'gemini-3-flash-preview', messages: history),
);
// ... send to Gemini, convert response, add to history.
// Turn 3: Switch back to GPT - full context preserved.
history.add(oai.ChatMessage.user('Which did we discuss first?'));
final gptResponse2 = await openAIClient.chat.completions.create(
oai.ChatCompletionCreateRequest(model: 'gpt-4.1-nano', messages: history),
);| Class | Description |
|---|---|
ChatCompletionRequestConverter |
Converts ChatCompletionCreateRequest → Gemini request components |
MessageContentConverter |
Bidirectional message/content conversion |
ChatCompletionResponseConverter |
Converts GenerateContentResponse → ChatCompletion |
GeminiStreamEventTransformer |
StreamTransformer for streaming responses |
convertGeminiStream() |
Convenience function for streaming with signature capture |
| Class | Description |
|---|---|
ToolMapper |
Tool definitions, tool choice, tool results, and schema sanitization |
FinishReasonMapper |
Maps Gemini finish reasons to OpenAI equivalents |
| OpenAI | Gemini | Notes |
|---|---|---|
role: "system" |
systemInstruction |
Extracted from messages to top-level field |
role: "assistant" |
role: "model" |
Direct rename |
role: "tool" |
role: "user" + FunctionResponsePart |
Role changes; consecutive tool messages merged |
max_tokens |
generationConfig.maxOutputTokens |
|
stop |
generationConfig.stopSequences |
|
tools |
tools[{functionDeclarations}] |
Schemas sanitized for Gemini compatibility |
tool_choice: "auto" |
functionCallingConfig.mode: AUTO |
|
tool_choice: "required" |
functionCallingConfig.mode: ANY |
|
tool_choice: "none" |
functionCallingConfig.mode: NONE |
|
response_format (json) |
responseMimeType: "application/json" |
|
reasoning_effort |
thinkingConfig.thinkingLevel |
low/medium/high mapped directly |
finish_reason: "stop" |
STOP |
|
finish_reason: "length" |
MAX_TOKENS |
|
finish_reason: "content_filter" |
SAFETY / RECITATION / BLOCKLIST / SPII |
Multiple Gemini reasons map to one |
finish_reason: "tool_calls" |
(tool calls detected) | Inferred from response content |
Gemini rejects standard JSON Schema combinators. ToolMapper.sanitizeSchema() automatically handles:
anyOf/oneOfwithconstvalues →enumarrayanyOf/oneOfwith object variants → mergedpropertiesallOf→ merged into single schemaconst→ single-valueenum- Strips
strict,additionalProperties,$schema
MIT