Skip to content

Native API Key Strategy — Investigation Report

This document records the findings from the native API key strategy investigation (June 2026). It answers whether SwarmForge should keep its unified OpenAI-compatible client for native OpenAI and Gemini keys, or adopt per-provider SDKs—with emphasis on multi-turn tool calling.

For setup instructions, see Providers.

Executive Summary

ProviderRecommendationRationale
OpenAI (native key)Keep unified OpenAI SDKAlready the native path (api.openai.com/v1). No separate SDK needed.
Gemini (native key)Keep OpenAI-compat for simple tool loops; plan native adapter if Gemini 3+ agents or Google built-in tools are requiredOpenAI-compat works for basic scenarios; Google recommends google-genai for agentic multi-turn flows.
OpenRouterKeep as primary multi-provider pathNormalizes provider quirks; all probe scenarios passed for both OpenAI and Gemini model slugs.

Architecture decision: Option 1 — Keep unified OpenAI-compatible layer with targeted provider quirks centralized in the turn runner and conversation runner. Add a native google-genai adapter (Option 2) only when Gemini 3 thought signatures, built-in Google tools, or Interactions API state are first-class requirements.

Current Architecture

SwarmForge routes all hosted model calls through one stack:

ModelConfig → OpenAIClientWrapper (openai SDK) → OpenAICompatibleTurnRunner → Orchestrator
ProviderEnv keyBase URL
OpenAIOPENAI_API_KEYhttps://api.openai.com/v1
GeminiGEMINI_API_KEY / GOOGLE_API_KEYhttps://generativelanguage.googleapis.com/v1beta/openai/
OpenRouterOPENROUTER_API_KEYhttps://openrouter.ai/api/v1

Tools are always defined in OpenAI tools format (src/swarmforge/swarm/tools.py). There is no google-genai, Anthropic, or LiteLLM dependency.

Probe Methodology

The probe script scripts/provider_probe.py exercises seven scenarios per provider:

  1. Single-turn chat
  2. Single tool call + follow-up (role: tool)
  3. Multi-turn tool loop via ConversationRunner
  4. Parallel tool calls in one assistant turn
  5. Streaming with tools
  6. Structured output (Pydantic .parse())
  7. Orchestrator tool loop via process_swarm_stream

Run locally:

bash
python3 scripts/provider_probe.py
python3 scripts/provider_probe.py --provider gemini --model gemini-3.1-flash-lite
python3 scripts/provider_probe.py --output /tmp/probe-results.json

Compatibility Matrix Results

Probe run: 2026-06-29 (results in scripts/provider_probe_results.json).

ScenarioOpenAI directGemini directOpenRouter openai/gpt-4o-miniOpenRouter google/gemini-3.1-flash-lite
single_turn_chatskipped (no key)skipped (no key)passpass
single_tool_call_role_toolskippedskippedpasspass
multi_turn_tool_loop_role_toolskippedskippedpass (after fix)pass
parallel_tool_callsskippedskippedpasspass
streaming_with_toolsskippedskippedpasspass
structured_outputskippedskippedpasspass
orchestrator_tool_loopskippedskippedpasspass

Note: Native OPENAI_API_KEY and GEMINI_API_KEY were not available in the probe environment. OpenRouter results proxy OpenAI and Gemini behavior. Re-run with native keys before production sign-off on direct Gemini endpoints.

Pre-fix failure (resolved)

ConversationRunner multi-turn tool loops failed on strict OpenAI providers with:

Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.

Root cause: Tool result messages were appended to conversation_history without first persisting the assistant message that issued tool_calls. The orchestrator already did this correctly; only the evaluation ConversationRunner was affected.

Fix: Append assistant tool_calls to history before tool results (src/swarmforge/evaluation/runner/conversation.py).

Issue Classification

IssueSeverityClassStatus
Missing assistant tool_calls in ConversationRunner historyBlocker (OpenAI strict)A — Fixable in unified layerFixed
Runtime-only tool fields sent to Gemini (mock_response, handler)Blocker (Gemini strict)A — Fixable in unified layerFixed in OpenAIClientWrapper via sanitize_tool_definitions
Inconsistent Gemini tool-result role (function vs tool)MediumA — Fixable in unified layerOpen: ConversationRunner uses role: function for Gemini; orchestrator and turn runner use role: tool
Empty assistant content when tool_calls presentMedium (Gemini direct)A — FixableOpen: orchestrator uses content: response_text or None; may need "[TOOL CALL]" placeholder for native Gemini
Gemini 3 thought_signature round-tripHigh (Gemini 3+ agents)A partial / B native SDKFixed for ConversationRunner and orchestrator via serialize_assistant_message_for_history
tools_format=gemini authoring vs runtimeLowA — Doc onlyConfirmed: authoring emits FunctionDeclaration shape; runtime expects OpenAI function.name
Built-in Google tools (Search, MCP, code execution)N/A todayB — Requires native SDKOut of scope for unified layer
Streaming tool-call deltasLowA — WorksPassed for OpenRouter OpenAI and Gemini slugs

Classification key:

  • A — Fixable in unified OpenAI-compat layer (message shaping, passthrough fields)
  • B — Requires google-genai provider adapter or Interactions API
  • C — Fundamental OpenAI-compat limitation; use OpenRouter proxy or native SDK

Google OpenAI-Compatibility vs Native SDK

Per Gemini OpenAI compatibility:

FactorUnified OpenAI-compatNative google-genai
Google's recommendation for new workBridge for existing OpenAI codePreferred for agents and multi-turn tools
MaturityBeta; unlisted params silently ignoredGA SDK
Basic function callingSupportedSupported
Multi-turn tool loopsWorks with message-order fixesSDK handles response objects automatically
Gemini 3 thought signaturesManual extra_content.google.thought_signatureAutomatic when appending full model response
Built-in toolsLimited / extra_bodyFirst-class

For OpenAI native keys, there is no compat-vs-native tradeoff—the OpenAI SDK against api.openai.com is already native.

Architecture Options — Scored

Criterion (weight)Option 1: Unified OpenAI-compatOption 2: Provider adapterOption 3: OpenRouter for non-OpenAI
Multi-turn tool reliability (high)8/10 after ConversationRunner fix9/109/10
Single tool schema everywhere (high)10/106/108/10
Gemini feature parity (medium)5/109/107/10
Maintenance cost (medium)7/105/108/10
Native key requirement (low)10/1010/103/10
Weighted score8.07.27.5

Fix provider quirks centrally in turn_runners.py, conversation.py, and orchestrator message persistence. One SDK, one tool format.

When sufficient: OpenAI direct keys, Gemini simple tool loops, OpenRouter multi-provider routing.

Option 2 — Provider adapter layer

Keep OpenAI-compat for OpenAI/OpenRouter; add google-genai behind the existing AgentTurnRunner protocol.

When needed: Gemini 3+ multi-turn agents, thought signatures, built-in Google tools, Interactions API state.

Option 3 — Hybrid routing

Direct OpenAI keys; route Gemini/Anthropic through OpenRouter.

When needed: Production swarms where direct Gemini key quirks are unacceptable but native keys for OpenAI are required.

PriorityPatchFile(s)
DoneStrip runtime-only tool fields before API requestsevaluation/provider/client.py
DonePersist assistant tool_calls before tool results in evaluation runnerevaluation/runner/conversation.py
HighCentralize Gemini tool-result role (function vs tool) in turn runnerswarm/turn_runners.py
MediumNon-empty assistant content when persisting tool_calls for Geminiswarm/orchestrator.py
MediumCapture and replay thought_signature from assistant responsesevaluation/provider/messages.py, conversation.py, turn_runners.py, orchestrator.py
LowDocument that tools_format=gemini is authoring-output onlydocs/authoring.md, docs/providers.md

Test Coverage

Env-gated integration tests live in tests/test_provider_compatibility_integration.py. They skip automatically when provider API keys are missing.

Unit test for ConversationRunner tool-history ordering: tests/test_conversation_runner.py.

References

Released as open source.