LaunchKit

Seventeen providers, one code path, and the credentials that refuse to be uniform

September 21, 2026

The promise of a gem like ruby_llm is that providers become interchangeable. In the chat call they are: the same ask reaches Anthropic, OpenAI, Gemini, Bedrock, Vertex AI or a model running on your laptop, and the reply comes back in the same shape.

The seam is one layer down, in what each provider wants before it will answer at all.

Asking the gem instead of guessing

ruby_llm providers are not a list you have to keep: every provider class declares what it needs, and the registry is readable at runtime. That matters more than it sounds, because the set changes between releases and a hardcoded copy of it goes stale without anything failing. Reading it is four lines in a console and the answer is current by construction:

RubyLLM::Provider.providers.each do |name, klass|
  puts "#{name}: #{klass.configuration_requirements.inspect}"
end

Seventeen entries come back. Twelve of them say exactly what you expect, one API key, which is why ruby_llm configuration looks trivial until it is not:

anthropic: [:anthropic_api_key]      openai: [:openai_api_key]
gemini: [:gemini_api_key]            mistral: [:mistral_api_key]
deepseek: [:deepseek_api_key]        perplexity: [:perplexity_api_key]
openrouter: [:openrouter_api_key]    xai: [:xai_api_key]
cohere: [:cohere_api_key]            deepgram: [:deepgram_api_key]
elevenlabs: [:elevenlabs_api_key]    ollama_cloud: [:ollama_cloud_api_key]

The other five are the ones that break a settings screen built on the assumption above.

The five that want something else

azure:    [:azure_api_base]
ollama:   [:ollama_api_base]
gpustack: [:gpustack_api_base]
bedrock:  [:bedrock_region]
vertexai: [:vertexai_project_id, :vertexai_location]

Azure OpenAI wants an endpoint, because the model lives at a URL you deployed rather than at a vendor's. Its azure_api_key is optional, sitting alongside azure_ai_auth_token, so a key is one of two ways in rather than the way in.

Ollama and GPUStack want an address and no key at all, which is the whole point of running a model yourself. A settings form with a required password field for these is a form that asks you to invent a secret.

Bedrock and Vertex AI are the interesting pair, and they are interesting for the same reason. Both require the least of any provider in the list, and neither requires a secret.

Why the two big clouds ask for the least

Bedrock requires a region. Not a key, not a secret, not a session token, all three of which are declared optional next to bedrock_credential_provider.

Vertex AI requires a project id and a location. Its vertexai_service_account_key is optional in exactly the same way.

The reason is that both platforms already have an answer to "who are you" that does not involve pasting a secret into an application. An EC2 instance or an ECS task has a role, a GKE workload has a service account, and a developer's laptop has whatever aws configure and gcloud auth left behind. Requiring a key would mean refusing to work in the environment these services were designed for.

What they cannot infer is where. A region and a project are not credentials, they are addresses, and no ambient credential chain can guess which one you meant.

That distinction is worth holding onto when you build the form. A region and a project id belong in plain text inputs. Masking them, storing them encrypted, and never echoing them back is effort spent protecting values that appear in every log line and every ARN.

Local providers skip the registry

A provider class declares one more thing, and a Rails Ollama setup is the reason to care:

RubyLLM::Provider.providers.count { |_name, klass| klass.local? }  # => 2

local? is true for Ollama and GPUStack, and it changes behaviour rather than labelling. Model resolution checks the registry to reject a typo in a model id, and a local provider has no registry to check because the models are whatever you installed. So the gem assumes a model on a local provider exists.

The same escape hatch is available explicitly, which is what makes a local provider usable as a test harness for everything else:

chat.with_model("anything-you-like", provider: :ollama, assume_model_exists: true)

Point that at a small server of your own speaking the OpenAI chat-completions dialect, and the whole Rails LLM provider path runs with no key, no network and no bill: failing over to a second provider is easier to test that way than against a real provider, because you can make the failure happen on demand.

The mistake this invites

Given a list like the one above, the tempting move is to copy it into your own application as a constant, so the settings screen knows which fields to render. That is what we did, and the copy was wrong within one release: our hardcoded Bedrock entry lists an access key id, a secret access key, a region and a session token, and the gem's own option names for three of those are bedrock_api_key, bedrock_secret_key and bedrock_session_token.

The list is readable at runtime, from configuration_requirements and configuration_options, and reading it is the version that cannot drift. A copy is a second source of truth for something the gem changes on its own schedule, and the failure mode is a form that collects the wrong fields and a provider that stays unconfigured for a reason nobody can see in the UI.

What this page does not cover

Which provider to choose. Latency, price and model quality are the real inputs to that decision, they move monthly, and a page that ranked them would be wrong by the time it was indexed.

Nor does it cover the non-chat providers in the list. Deepgram is speech to text and ElevenLabs is text to speech, so both appear in the seventeen and neither answers a chat call. Counting every entry in the registry as a chat provider is the easiest mistake to make from the number alone.

Keep reading

← All Rails and AI articles