Dart & Flutter

Dart has no first-party OpenAI SDK. This page uses openai_dart, the community-maintained client from the langchain.dart project. It takes a baseUrl and an API key, and nothing else about your code changes. The provider key stays in the vault.

Install

dart pub add openai_dart

Configure

import 'dart:io';
import 'package:openai_dart/openai_dart.dart';

final org = Platform.environment['VULNETIX_ORG_UUID']!;

final client = OpenAIClient(
  config: OpenAIConfig(
    authProvider: ApiKeyProvider(Platform.environment['VULNETIX_API_KEY']!), // Vulnetix key
    baseUrl: 'https://guardrails.vulnetix.com/openai/$org/v1',
  ),
);

final response = await client.chat.completions.create(
  ChatCompletionCreateRequest(
    model: 'gpt-4o-mini',
    messages: [ChatMessage.user('Hello')],
  ),
);
print(response.text);

Swap openai in the path for any other provider slug and pass that provider’s own model string — the gateway forwards model verbatim.

Note Older versions of openai_dart construct the client as OpenAIClient(apiKey: …, baseUrl: …). The two settings are the same; only the constructor shape moved.

Streaming

Unchanged. The gateway relays the provider’s SSE stream.

final stream = client.chat.completions.createStream(
  ChatCompletionCreateRequest(
    model: 'gpt-4o-mini',
    messages: [ChatMessage.user('Hello')],
  ),
);

await for (final delta in stream.textDeltas()) {
  stdout.write(delta);
}

Handling a guardrail block

A request your policy refuses never reaches the provider. It returns an OpenAI-shaped 403, which the client throws as an ApiException carrying the status code and the response body.

try {
  final response = await client.chat.completions.create(request);
} on ApiException catch (e) {
  if (e.statusCode == 403) {
    // The body is the firewall's error envelope:
    //   {"error": {"code": "request_blocked", "blocked_by": "<rule>", ...}}
    final error = jsonDecode(e.message)['error'] as Map<String, dynamic>?;
    final code = error?['code'];        // request_blocked, model_denied, ...
    final rule = error?['blocked_by'];  // the rule that stopped it

    throw StateError('AI firewall refused this request: $code (rule: $rule)');
  }
  rethrow;
}
codeMeaning
request_blockedA content guardrail matched. blocked_by names the rule; violations lists every rule that matched.
provider_deniedPolicy does not allow this provider.
model_denied / model_not_allowedPolicy does not allow this model.
provider_key_missingNo provider key in the vault for this provider.

If the exception in your version exposes the body under a different field, the envelope itself is unchanged — see curl & raw HTTP for its exact shape.

Note Redaction is silent — nothing is thrown. A pii_redact rule rewrites the matched spans in your prompt to the literal [REDACTED] and forwards the request. You get an ordinary completion back, about redacted text.

Tool / function calling

Tools are forwarded untouched. Guardrails inspect the request you send, which includes the tool-result messages you append before the next call.

Gotchas

  • Do not put the Vulnetix key in a Flutter app. Anything shipped to a device is extractable, and a leaked key lets an attacker spend your provider quota through your organisation’s policy. Call the gateway from a backend you control, and have the app talk to that backend.
  • api_key is the Vulnetix key, not a provider key — the most common 401.
  • baseUrl must include the /v1 suffix.
  • This is a community package. If you use a different one (dart_openai, for instance), check whether it appends /v1 itself before you add it to the base URL: the request must land on …/{orgUuid}/v1/chat/completions.