mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: use AI provider chat APIs (#25415)
This commit is contained in:
@@ -350,13 +350,13 @@ func (p *Server) resolveAdvisorModelOverride(
|
||||
return fallbackModel, fallbackCallConfig
|
||||
}
|
||||
|
||||
// GetEnabledChatModelConfigByID joins on chat_providers.enabled = TRUE
|
||||
// and chat_model_configs.enabled = TRUE, so it returns sql.ErrNoRows
|
||||
// the moment an admin disables either the model config or its provider.
|
||||
// Using the cached ModelConfigByID here would keep resolving an override
|
||||
// whose provider was just disabled, and an env or central fallback key
|
||||
// would let ModelFromConfig succeed, silently routing advisor prompts
|
||||
// to a provider the admin expects to be off.
|
||||
// GetEnabledChatModelConfigByID checks the model config and referenced
|
||||
// provider enabled state, so it returns sql.ErrNoRows the moment an
|
||||
// admin disables either one. Using the cached ModelConfigByID here
|
||||
// would keep resolving an override whose provider was just disabled,
|
||||
// and an available fallback key would let ModelFromConfig succeed,
|
||||
// silently routing advisor prompts to a provider the admin expects to
|
||||
// be off.
|
||||
overrideConfig, err := p.db.GetEnabledChatModelConfigByID(
|
||||
ctx,
|
||||
advisorCfg.ModelConfigID,
|
||||
|
||||
@@ -288,22 +288,7 @@ func TestSubagentChatExcludesWorkspaceProvisioningTools(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
_, err := expClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
APIKey: "test-api-key",
|
||||
BaseURL: openAIURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
isDefault := true
|
||||
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
IsDefault: &isDefault,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL)
|
||||
|
||||
// Create a root chat whose first model call will spawn a subagent.
|
||||
chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
@@ -483,22 +468,7 @@ func TestPlanModeSubagentChatExcludesAskUserQuestion(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
_, err = expClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
APIKey: "test-api-key",
|
||||
BaseURL: openAIURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
isDefault := true
|
||||
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
IsDefault: &isDefault,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL)
|
||||
|
||||
chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
OrganizationID: user.OrganizationID,
|
||||
@@ -638,24 +608,9 @@ func TestExploreSubagentIsReadOnly(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
_, err := expClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
APIKey: "test-api-key",
|
||||
BaseURL: openAIURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
isDefault := true
|
||||
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
IsDefault: &isDefault,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = expClient.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
_, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
OrganizationID: user.OrganizationID,
|
||||
WorkspaceID: &workspace.ID,
|
||||
Content: []codersdk.ChatInputPart{
|
||||
@@ -4953,22 +4908,7 @@ func TestCreateWorkspaceTool_EndToEnd(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
_, err := expClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
APIKey: "test-api-key",
|
||||
BaseURL: openAIURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
isDefault := true
|
||||
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
IsDefault: &isDefault,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL)
|
||||
|
||||
chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
OrganizationID: user.OrganizationID,
|
||||
@@ -5123,22 +5063,7 @@ func TestStartWorkspaceTool_EndToEnd(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
_, err := expClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
APIKey: "test-api-key",
|
||||
BaseURL: openAIURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
isDefault := true
|
||||
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
IsDefault: &isDefault,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL)
|
||||
|
||||
// Create a chat with the stopped workspace pre-associated.
|
||||
chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
@@ -8586,22 +8511,7 @@ func TestAgentContextFilesAndSkillsLoadedIntoChat(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
_, err := expClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
APIKey: "test-api-key",
|
||||
BaseURL: openAIURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
contextLimit := int64(4096)
|
||||
isDefault := true
|
||||
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: "openai-compat",
|
||||
Model: "gpt-4o-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
IsDefault: &isDefault,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.CreateOpenAICompatChatModelConfig(t, expClient, openAIURL)
|
||||
|
||||
workspaceID := workspace.ID
|
||||
chat, err := expClient.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
@@ -13,6 +14,52 @@ import (
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func createIntegrationAIProvider(
|
||||
ctx context.Context,
|
||||
t testing.TB,
|
||||
client *codersdk.ExperimentalClient,
|
||||
providerType codersdk.AIProviderType,
|
||||
apiKey string,
|
||||
baseURL string,
|
||||
) codersdk.AIProvider {
|
||||
t.Helper()
|
||||
if baseURL == "" {
|
||||
baseURL = defaultIntegrationAIProviderBaseURL(providerType)
|
||||
}
|
||||
provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
|
||||
Type: providerType,
|
||||
Name: string(providerType) + "-" + uuid.NewString(),
|
||||
DisplayName: aiProviderDisplayName(providerType),
|
||||
Enabled: true,
|
||||
BaseURL: baseURL,
|
||||
APIKeys: []string{apiKey},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return provider
|
||||
}
|
||||
|
||||
func defaultIntegrationAIProviderBaseURL(providerType codersdk.AIProviderType) string {
|
||||
switch providerType {
|
||||
case codersdk.AIProviderTypeAnthropic:
|
||||
return "https://api.anthropic.com"
|
||||
case codersdk.AIProviderTypeOpenAI:
|
||||
return "https://api.openai.com/v1"
|
||||
default:
|
||||
return "https://api.example.com"
|
||||
}
|
||||
}
|
||||
|
||||
func aiProviderDisplayName(providerType codersdk.AIProviderType) string {
|
||||
switch providerType {
|
||||
case codersdk.AIProviderTypeAnthropic:
|
||||
return "Anthropic"
|
||||
case codersdk.AIProviderTypeOpenAI:
|
||||
return "OpenAI"
|
||||
default:
|
||||
return string(providerType)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnthropicWebSearchRoundTrip is an integration test that verifies
|
||||
// provider-executed tool results (web_search) survive the full
|
||||
// persist → reconstruct → re-send cycle. It sends a query that
|
||||
@@ -43,19 +90,16 @@ func TestAnthropicWebSearchRoundTrip(t *testing.T) {
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
expClient := codersdk.NewExperimentalClient(client)
|
||||
|
||||
// Configure an Anthropic provider with the real API key.
|
||||
_, err := expClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
|
||||
Provider: "anthropic",
|
||||
APIKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
provider := createIntegrationAIProvider(
|
||||
ctx, t, expClient, codersdk.AIProviderTypeAnthropic, apiKey, baseURL,
|
||||
)
|
||||
|
||||
// Create a model config that enables web_search.
|
||||
contextLimit := int64(200000)
|
||||
isDefault := true
|
||||
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: "anthropic",
|
||||
_, err := expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: string(provider.Type),
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "claude-sonnet-4-20250514",
|
||||
ContextLimit: &contextLimit,
|
||||
IsDefault: &isDefault,
|
||||
@@ -303,13 +347,9 @@ func TestOpenAIReasoningRoundTrip(t *testing.T) {
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
expClient := codersdk.NewExperimentalClient(client)
|
||||
|
||||
// Configure an OpenAI provider with the real API key.
|
||||
_, err := expClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
|
||||
Provider: "openai",
|
||||
APIKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
provider := createIntegrationAIProvider(
|
||||
ctx, t, expClient, codersdk.AIProviderTypeOpenAI, apiKey, baseURL,
|
||||
)
|
||||
|
||||
// Create a model config for a reasoning model with Store: true
|
||||
// (the default). Using o4-mini because it always produces
|
||||
@@ -317,8 +357,9 @@ func TestOpenAIReasoningRoundTrip(t *testing.T) {
|
||||
contextLimit := int64(200000)
|
||||
isDefault := true
|
||||
reasoningSummary := "auto"
|
||||
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: "openai",
|
||||
_, err := expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: string(provider.Type),
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "o4-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
IsDefault: &isDefault,
|
||||
@@ -457,21 +498,18 @@ func TestOpenAIReasoningRoundTripStoreFalse(t *testing.T) {
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
expClient := codersdk.NewExperimentalClient(client)
|
||||
|
||||
// Configure an OpenAI provider with the real API key.
|
||||
_, err := expClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
|
||||
Provider: "openai",
|
||||
APIKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
provider := createIntegrationAIProvider(
|
||||
ctx, t, expClient, codersdk.AIProviderTypeOpenAI, apiKey, baseURL,
|
||||
)
|
||||
|
||||
// Create a model config for a reasoning model with Store: false.
|
||||
// Using o4-mini because it always produces reasoning items.
|
||||
contextLimit := int64(200000)
|
||||
isDefault := true
|
||||
reasoningSummary := "auto"
|
||||
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: "openai",
|
||||
_, err := expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
|
||||
Provider: string(provider.Type),
|
||||
AIProviderID: &provider.ID,
|
||||
Model: "o4-mini",
|
||||
ContextLimit: &contextLimit,
|
||||
IsDefault: &isDefault,
|
||||
|
||||
Reference in New Issue
Block a user