feat: route extra ai_provider_types through OpenAI and Anthropic providers (#25722)

_Disclosure:_ _produced_ _with_ _Claude_ _Opus_ _4\.7_

AI Gateway only supports Anthropic (+Bedrock), OpenAI, and Copilot providers at present. All other types (Vercel, Gemini, etc) will be mapped to OpenAI since they support OpenAI-compatible endpoints.
This commit is contained in:
Danny Kopping
2026-05-27 16:16:05 +02:00
committed by GitHub
parent 6f06ace949
commit 2770bdc9d1
13 changed files with 243 additions and 64 deletions
+11 -8
View File
@@ -320,10 +320,13 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) {
if req.Settings != nil {
existing = mergeAIProviderSettings(existing, *req.Settings)
}
// Bedrock settings are only meaningful for anthropic-typed
// providers; rejecting the mismatch keeps a misconfiguration
// from sitting silently in the encrypted blob.
if existing.Bedrock != nil && old.Type != database.AiProviderTypeAnthropic {
// Bedrock settings are only meaningful for anthropic- or
// bedrock-typed providers; rejecting the mismatch keeps a
// misconfiguration from sitting silently in the encrypted
// blob.
if existing.Bedrock != nil &&
old.Type != database.AiProviderTypeAnthropic &&
old.Type != database.AiProviderTypeBedrock {
return errAIProviderBedrockTypeMismatch
}
settings, err := encodeAIProviderSettings(existing)
@@ -382,7 +385,7 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) {
}
if errors.Is(err, errAIProviderBedrockTypeMismatch) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Bedrock settings are only valid for type=anthropic.",
Message: "Bedrock settings are only valid for type=anthropic or type=bedrock.",
})
return
}
@@ -482,9 +485,9 @@ var errBedrockRejectsAPIKeys = xerrors.New("bedrock providers do not accept api_
// errAIProviderBedrockTypeMismatch is the sentinel returned from
// inside the update transaction when the post-merge settings carry a
// Bedrock block but the provider is not anthropic-typed; the outer
// handler translates it into a 400.
var errAIProviderBedrockTypeMismatch = xerrors.New("bedrock settings are only valid for type=anthropic")
// Bedrock block but the provider is not anthropic- or bedrock-typed;
// the outer handler translates it into a 400.
var errAIProviderBedrockTypeMismatch = xerrors.New("bedrock settings are only valid for type=anthropic or type=bedrock")
// errAIProviderInvalidName is returned from lookupAIProvider when the
// idOrName parameter is neither a UUID nor a syntactically-valid name.
+8 -13
View File
@@ -327,28 +327,23 @@ func providersFromEnv(ctx context.Context, cfg codersdk.AIBridgeConfig, logger s
dp := desiredAIProvider{
Name: name,
}
switch p.Type {
case aibridge.ProviderOpenAI:
dp.Type = database.AiProviderTypeOpenai
case aibridge.ProviderAnthropic:
dp.Type = database.AiProviderTypeAnthropic
case aibridge.ProviderCopilot:
dp.Type = database.AiProviderTypeCopilot
default:
providerType := database.AIProviderType(p.Type)
if !providerType.Valid() {
logger.Warn(ctx, "skipping indexed AI provider with unsupported type",
slog.F("name", name),
slog.F("type", p.Type),
)
continue
}
dp.Type = providerType
dp.BaseURL = p.BaseURL
// Bedrock fields only apply to Anthropic. Detection goes
// through AIProviderBedrockSettings.IsConfigured() so the
// legacy and indexed paths agree on what counts as a Bedrock
// provider.
// Bedrock fields apply to Anthropic and the dedicated Bedrock
// type. Detection goes through
// AIProviderBedrockSettings.IsConfigured() so the legacy and
// indexed paths agree on what counts as a Bedrock provider.
isBedrock := false
if dp.Type == database.AiProviderTypeAnthropic {
if dp.Type == database.AiProviderTypeAnthropic || dp.Type == database.AiProviderTypeBedrock {
var accessKey, accessKeySecret string
if len(p.BedrockAccessKeys) > 0 {
accessKey = p.BedrockAccessKeys[0]
+6 -5
View File
@@ -371,14 +371,15 @@ func TestSeedAIProvidersFromEnv(t *testing.T) {
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)
// vercel is a valid ai_provider_type DB value but the aibridge
// runtime has no constructor for it, so the seed switch falls
// into the default branch and skips the row.
// A TYPE that isn't part of the ai_provider_type enum falls
// into the default branch and the row is skipped rather than
// rejected, so deployments don't fail to start over a single
// typo'd provider.
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIProviderConfig{
{
Type: "vercel",
Name: "vercel-instance",
Type: "not-a-real-provider",
Name: "ghost",
BaseURL: "https://example.com",
},
{
+1 -1
View File
@@ -15071,7 +15071,7 @@ const docTemplate = `{
"type": "string"
},
"type": {
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
"description": "Type is the provider type. Valid values are: \"openai\",\n\"anthropic\", \"azure\", \"bedrock\", \"google\", \"openai-compat\",\n\"openrouter\", \"vercel\", \"copilot\".",
"type": "string"
}
}
+1 -1
View File
@@ -13475,7 +13475,7 @@
"type": "string"
},
"type": {
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
"description": "Type is the provider type. Valid values are: \"openai\",\n\"anthropic\", \"azure\", \"bedrock\", \"google\", \"openai-compat\",\n\"openrouter\", \"vercel\", \"copilot\".",
"type": "string"
}
}