Skip to main content

gemini_openai_claude 1.0.0 copy "gemini_openai_claude: ^1.0.0" to clipboard
gemini_openai_claude: ^1.0.0 copied to clipboard

Structured JSON output from Gemini, OpenAI and Claude with one schema. Images, actionable errors, no vendor SDKs. Unofficial.

gemini_openai_claude #

Structured JSON output from Gemini, OpenAI and Claude with one schema. Write the request once; each adapter converts it to its vendor's dialect and returns the decoded JSON object. Images supported. Errors come back as kinds your UI can act on. No vendor SDKs, one dependency (http). Works in Flutter and in plain Dart.

import 'package:gemini_openai_claude/gemini_openai_claude.dart';

final spec = PromptSpec(
  instruction: 'Suggest a title and three tags for this note: "Fix the bike brakes before Sunday."',
  schema: {
    'type': 'object',
    'properties': {
      'title': {'type': 'string'},
      'tags': {'type': 'array', 'items': {'type': 'string'}},
    },
    'required': ['title', 'tags'],
  },
);

final AiAdapter ai = GeminiProvider(); // or OpenAiProvider(), ClaudeProvider()
final json = await ai.complete(spec: spec, apiKey: myKey);
print(json['title']);

Unofficial. Not affiliated with Google, OpenAI or Anthropic.

Why #

The three vendors all support structured output, but each wants the same schema in a different dialect, and each fails differently:

Gemini OpenAI Claude
Schema types STRING, OBJECT (upper case) lower case lower case
Objects as written additionalProperties: false on every object additionalProperties: false on every object
Optional fields as written not allowed in strict mode: every property required, optional ones nullable (and null added to any enum) as written
Images inline_data after the text image_url data URL base64 image blocks before the text

PromptSpec holds the transforms (geminiSchema, sealedSchema, strictSchema, and the content shapes), so a fix lands for all three at once. The adapters stay thin.

The other half is failures that arrive as HTTP 200: a safety block, an answer cut off at the token limit, a refusal, an empty candidate list. Each adapter names them instead of reporting "the API may have changed".

Install #

dart pub add gemini_openai_claude   # or: flutter pub add gemini_openai_claude

Images #

final spec = PromptSpec(
  instruction: 'Read the total and the date on this receipt.',
  schema: {
    'type': 'object',
    'properties': {
      'total': {'type': 'number'},
      'date': {'type': 'string'},
    },
  },
  images: [
    PromptImage(mime: 'image/jpeg', base64: base64Encode(bytes), label: 'receipt'),
  ],
);

Use JPEG or PNG. Convert HEIC first.

Errors #

Every failure is a ProviderException with a kind, a user-facing message and the vendor's raw detail:

try {
  final json = await ai.complete(spec: spec, apiKey: key, model: model);
} on ProviderException catch (e) {
  switch (e.kind) {
    case ProviderErrorKind.modelNotFound: // the model name was retired: ask for a current one
    case ProviderErrorKind.invalidKey:
    case ProviderErrorKind.missingKey:
      openSettings();
    case ProviderErrorKind.truncated:     // hit the output limit: retrying usually fits
    case ProviderErrorKind.busy:          // vendor overloaded (5xx): wait and retry
      showRetry(e.message);
    default:
      showError(e.message, details: e.detail);
  }
}
Kind Typical cause
missingKey · invalidKey No key, or the vendor rejected it
forbidden Key is fine, but the API is not enabled on the project, or the region is blocked (Gemini answers 403)
modelNotFound Model name retired or misspelled
quota Rate limit or billing (429)
busy Vendor overloaded (5xx, Claude 529)
declined Safety block or refusal, reported as 200
truncated Output token limit reached
emptyResult · badResponse Nothing usable came back
network No connection, or no answer within providerTimeout (120 s)

e.pointsAtSettings is true when the fix is a setting (key, model, account), so you know when to show a Settings button.

Messages are English. To translate:

ProviderException.localize = (kind, args) => switch (kind) {
  ProviderErrorKind.missingKey => 'Ajoutez votre clé ${args['name']}.',
  _ => null, // keep the English default
};

Models are a setting #

Vendors retire model names roughly once a year. Each adapter has a defaultModel (GeminiProvider.defaultModel, …) used when you pass model: '', but let your users or your remote config change it. When a name stops working you get modelNotFound, not a crash.

Demo mode #

DemoProvider answers on the device with no key and no network. Give the spec an offline answer built from the same inputs:

final spec = PromptSpec(
  instruction: '...',
  schema: schema,
  demo: () => {'title': 'Bike brakes', 'tags': ['bike', 'repair', 'weekend']},
);
await const DemoProvider().complete(spec: spec, apiKey: '');

Use it for demo builds, screenshots and UI work.

Do not ship your API key in the app #

Anything in an app binary can be extracted. Letting users paste their own key (stored in the Keychain / Keystore) is fine, and that is what these adapters are built for. If your key pays for the requests, put it on a server and have the app call your server; run these adapters there (the package is plain Dart) or use the vendor's server SDK.

Testing #

Every adapter takes an http.Client, so tests run without a network:

final ai = GeminiProvider(
  client: MockClient((req) async => http.Response(jsonEncode({
    'candidates': [
      {'content': {'parts': [{'text': '{"title":"ok","tags":[]}'}]}},
    ],
  }), 200)),
);

FAQ #

How do I get structured JSON output from Gemini, OpenAI and Claude with the same schema? Write one JSON Schema in a PromptSpec. GeminiProvider sends it as responseSchema with upper-case types, OpenAiProvider as a strict json_schema with every property required, ClaudeProvider with additionalProperties: false on every object.

Why does OpenAI reject my schema with a 400? Strict structured output requires every property in required and additionalProperties: false. Optional properties must be nullable, and a nullable enum must list null. strictSchema does all of that.

Why does Gemini return 403 with a valid key? The Generative Language API is not enabled on the key's project, or the region is not supported. You get forbidden, not invalidKey.

Is this a chat SDK? No. One request, one structured answer. For chat and streaming use the vendor SDKs.

About #

Extracted from the AI apps in the coderqh Flutter app templates. Issues and pull requests are welcome.

MIT licensed.

1
likes
160
points
0
downloads

Documentation

API reference

Publisher

verified publishercoderqh.com

Weekly Downloads

Structured JSON output from Gemini, OpenAI and Claude with one schema. Images, actionable errors, no vendor SDKs. Unofficial.

Homepage
Repository (GitHub)
View/report issues

Topics

#ai #llm #gemini #openai #claude

License

MIT (license)

Dependencies

http

More

Packages that depend on gemini_openai_claude