mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
306 lines
11 KiB
Go
306 lines
11 KiB
Go
package codersdk
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
// AIProviderType identifies the protocol Coder uses to communicate
|
|
// with an upstream AI provider.
|
|
type AIProviderType string
|
|
|
|
const (
|
|
AIProviderTypeOpenAI AIProviderType = "openai"
|
|
AIProviderTypeAnthropic AIProviderType = "anthropic"
|
|
// AIProviderTypeAzure, AIProviderTypeGoogle, AIProviderTypeOpenAICompat,
|
|
// AIProviderTypeOpenrouter, and AIProviderTypeVercel route through
|
|
// aibridge's OpenAI client today because chatd configures these
|
|
// providers against their OpenAI-compatible endpoints. Native
|
|
// gateway-side support arrives later without an enum change.
|
|
AIProviderTypeAzure AIProviderType = "azure"
|
|
AIProviderTypeGoogle AIProviderType = "google"
|
|
AIProviderTypeOpenAICompat AIProviderType = "openai-compat"
|
|
AIProviderTypeOpenrouter AIProviderType = "openrouter"
|
|
AIProviderTypeVercel AIProviderType = "vercel"
|
|
// AIProviderTypeBedrock routes through aibridge's Anthropic client
|
|
// using the Bedrock discriminator in Settings; native support is
|
|
// future work.
|
|
AIProviderTypeBedrock AIProviderType = "bedrock"
|
|
)
|
|
|
|
// AIProviderSettings is the discriminated container for type-specific
|
|
// provider settings stored in ai_providers.settings. Providers that
|
|
// need no type-specific configuration (current OpenAI and standard
|
|
// Anthropic flows) leave every field nil; the wire form for those
|
|
// providers is JSON null.
|
|
//
|
|
// On the wire, settings serialize as a JSON object that always carries
|
|
// _type and _version discriminator keys alongside the type-specific
|
|
// fields. The custom (Un)MarshalJSON implementations on this type
|
|
// handle the routing automatically; callers should never marshal the
|
|
// concrete settings struct directly.
|
|
type AIProviderSettings struct {
|
|
// Bedrock, when set, indicates this provider authenticates against
|
|
// AWS Bedrock instead of api.anthropic.com. Only meaningful for
|
|
// AIProviderTypeAnthropic.
|
|
Bedrock *AIProviderBedrockSettings `json:"-"`
|
|
}
|
|
|
|
// IsZero reports whether the settings carry no type-specific data.
|
|
func (s AIProviderSettings) IsZero() bool {
|
|
return s.Bedrock == nil
|
|
}
|
|
|
|
// MarshalJSON emits the discriminated wire form. Empty settings encode
|
|
// as JSON null so the column round-trips cleanly through SQL NULL.
|
|
func (s AIProviderSettings) MarshalJSON() ([]byte, error) {
|
|
switch {
|
|
case s.Bedrock != nil:
|
|
return marshalSettings(*s.Bedrock)
|
|
default:
|
|
return []byte("null"), nil
|
|
}
|
|
}
|
|
|
|
// UnmarshalJSON inspects the _type discriminator and routes to the
|
|
// concrete settings struct that matches it.
|
|
func (s *AIProviderSettings) UnmarshalJSON(data []byte) error {
|
|
*s = AIProviderSettings{}
|
|
trimmed := bytes.TrimSpace(data)
|
|
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
|
return nil
|
|
}
|
|
var header aiProviderSettingsHeader
|
|
if err := json.Unmarshal(data, &header); err != nil {
|
|
return xerrors.Errorf("decode settings header: %w", err)
|
|
}
|
|
if header.Type == "" {
|
|
return xerrors.New("settings missing _type discriminator")
|
|
}
|
|
switch header.Type {
|
|
case AIProviderSettingsTypeBedrock:
|
|
// TODO: handle multiple versions; this will be implemented
|
|
// once needed.
|
|
if header.Version != AIProviderBedrockSettingsVersion {
|
|
return xerrors.Errorf("unsupported %q settings version %d (expected %d)",
|
|
header.Type, header.Version, AIProviderBedrockSettingsVersion)
|
|
}
|
|
var b AIProviderBedrockSettings
|
|
if err := json.Unmarshal(data, &b); err != nil {
|
|
return xerrors.Errorf("decode bedrock settings: %w", err)
|
|
}
|
|
s.Bedrock = &b
|
|
return nil
|
|
default:
|
|
return xerrors.Errorf("unknown settings type %q", header.Type)
|
|
}
|
|
}
|
|
|
|
// aiProviderSettingsHeader is the discriminator-only view of an
|
|
// encoded settings blob.
|
|
type aiProviderSettingsHeader struct {
|
|
Type string `json:"_type"`
|
|
Version int `json:"_version"`
|
|
}
|
|
|
|
// settingsTyped is implemented by concrete settings structs so that
|
|
// marshalSettings can inject the discriminator without type-asserting
|
|
// against every variant.
|
|
type settingsTyped interface {
|
|
settingsType() string
|
|
settingsVersion() int
|
|
}
|
|
|
|
// marshalSettings encodes a concrete settings struct and merges the
|
|
// _type and _version discriminator keys at the top level of the
|
|
// resulting JSON object.
|
|
func marshalSettings(s settingsTyped) ([]byte, error) {
|
|
raw, err := json.Marshal(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var m map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return nil, err
|
|
}
|
|
if m == nil {
|
|
m = make(map[string]json.RawMessage)
|
|
}
|
|
typeRaw, err := json.Marshal(s.settingsType())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
versRaw, err := json.Marshal(s.settingsVersion())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m["_type"] = typeRaw
|
|
m["_version"] = versRaw
|
|
return json.Marshal(m)
|
|
}
|
|
|
|
// AIProvider represents an AI provider configuration row as returned
|
|
// by the API. API keys are stored in a separate ai_provider_keys
|
|
// table and managed via the keys sub-endpoints; secret fields on
|
|
// Settings are never included in responses.
|
|
type AIProvider struct {
|
|
ID uuid.UUID `json:"id" format:"uuid"`
|
|
Type AIProviderType `json:"type"`
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name"`
|
|
Enabled bool `json:"enabled"`
|
|
BaseURL string `json:"base_url"`
|
|
Settings AIProviderSettings `json:"settings"`
|
|
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
|
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
|
}
|
|
|
|
// CreateAIProviderRequest is the payload for creating a new AI
|
|
// provider. Name, Type, and BaseURL are required. API keys for
|
|
// OpenAI/Anthropic providers are added via the keys sub-endpoint
|
|
// after the provider is created; Bedrock providers carry their
|
|
// credentials in Settings and do not use the keys sub-endpoint.
|
|
type CreateAIProviderRequest struct {
|
|
Type AIProviderType `json:"type"`
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
Enabled bool `json:"enabled"`
|
|
BaseURL string `json:"base_url"`
|
|
Settings AIProviderSettings `json:"settings,omitzero"`
|
|
}
|
|
|
|
// UpdateAIProviderRequest is the payload for partially updating an
|
|
// AI provider. At least one field must be non-nil. Pointer fields
|
|
// distinguish "not sent" (nil) from "set to empty/zero" (a pointer
|
|
// to the zero value).
|
|
type UpdateAIProviderRequest struct {
|
|
DisplayName *string `json:"display_name,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
BaseURL *string `json:"base_url,omitempty"`
|
|
Settings *AIProviderSettings `json:"settings,omitempty"`
|
|
}
|
|
|
|
// AIProviderKey represents a single API key registered against an
|
|
// AI provider, as returned by the API. The plaintext APIKey is
|
|
// write-only and never included in responses.
|
|
type AIProviderKey struct {
|
|
ID uuid.UUID `json:"id" format:"uuid"`
|
|
ProviderID uuid.UUID `json:"provider_id" format:"uuid"`
|
|
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
|
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
|
}
|
|
|
|
// CreateAIProviderKeyRequest is the payload for adding an API key to
|
|
// an AI provider. Only meaningful for openai and anthropic providers;
|
|
// Bedrock providers reject this call because they use the access
|
|
// credentials stored in Settings.
|
|
type CreateAIProviderKeyRequest struct {
|
|
APIKey string `json:"api_key"`
|
|
}
|
|
|
|
// AIProviders lists all (non-deleted) AI providers.
|
|
func (c *Client) AIProviders(ctx context.Context) ([]AIProvider, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/ai/providers", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, ReadBodyAsError(res)
|
|
}
|
|
var providers []AIProvider
|
|
return providers, json.NewDecoder(res.Body).Decode(&providers)
|
|
}
|
|
|
|
// AIProvider fetches a single AI provider by ID or name.
|
|
func (c *Client) AIProvider(ctx context.Context, idOrName string) (AIProvider, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/ai/providers/%s", idOrName), nil)
|
|
if err != nil {
|
|
return AIProvider{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return AIProvider{}, ReadBodyAsError(res)
|
|
}
|
|
var provider AIProvider
|
|
return provider, json.NewDecoder(res.Body).Decode(&provider)
|
|
}
|
|
|
|
// CreateAIProvider creates a new AI provider.
|
|
func (c *Client) CreateAIProvider(ctx context.Context, req CreateAIProviderRequest) (AIProvider, error) {
|
|
res, err := c.Request(ctx, http.MethodPost, "/api/v2/ai/providers", req)
|
|
if err != nil {
|
|
return AIProvider{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusCreated {
|
|
return AIProvider{}, ReadBodyAsError(res)
|
|
}
|
|
var provider AIProvider
|
|
return provider, json.NewDecoder(res.Body).Decode(&provider)
|
|
}
|
|
|
|
// UpdateAIProvider partially updates an AI provider identified by
|
|
// ID or name.
|
|
func (c *Client) UpdateAIProvider(ctx context.Context, idOrName string, req UpdateAIProviderRequest) (AIProvider, error) {
|
|
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/ai/providers/%s", idOrName), req)
|
|
if err != nil {
|
|
return AIProvider{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return AIProvider{}, ReadBodyAsError(res)
|
|
}
|
|
var provider AIProvider
|
|
return provider, json.NewDecoder(res.Body).Decode(&provider)
|
|
}
|
|
|
|
// DeleteAIProvider soft-deletes an AI provider identified by ID or
|
|
// name. The row is preserved for audit/FK history.
|
|
func (c *Client) DeleteAIProvider(ctx context.Context, idOrName string) error {
|
|
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/ai/providers/%s", idOrName), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateAIProviderKey registers a new API key against an AI
|
|
// provider identified by ID or name.
|
|
func (c *Client) CreateAIProviderKey(ctx context.Context, idOrName string, req CreateAIProviderKeyRequest) (AIProviderKey, error) {
|
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/ai/providers/%s/keys", idOrName), req)
|
|
if err != nil {
|
|
return AIProviderKey{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusCreated {
|
|
return AIProviderKey{}, ReadBodyAsError(res)
|
|
}
|
|
var key AIProviderKey
|
|
return key, json.NewDecoder(res.Body).Decode(&key)
|
|
}
|
|
|
|
// DeleteAIProviderKey removes a single API key from an AI provider.
|
|
func (c *Client) DeleteAIProviderKey(ctx context.Context, idOrName string, keyID uuid.UUID) error {
|
|
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/ai/providers/%s/keys/%s", idOrName, keyID), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|