Skip to content

Commit 8cc399f

Browse files
NickGaganclaude
andauthored
feat: add external models endpoint (opendatahub-io#6465)
* feat(gen-ai): add external models endpoint Implement BFF endpoint to register external model endpoints (Gemini, OpenAI, Anthropic, vLLM). - Add POST /gen-ai/api/v1/models/external endpoint - Create ConfigMap (gen-ai-aa-external-models) for provider configuration - Create Kubernetes Secret for API key storage - Return AAModel response for compatibility with AI Available Assets - Add OpenAPI specification with ExternalModelCreatedResponse - Add unit tests and mock implementations - Support provider types: remote::gemini, remote::openai, remote::anthropic, remote::vllm - Support model types: llm, embedding Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix tests * coderabbit fixes * More coderabbit fixes * fix openAPI schema for AAModel * add 403 to /models/external * check for response.data in mock BFF test --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent e03606b commit 8cc399f

12 files changed

Lines changed: 1068 additions & 1 deletion

File tree

packages/gen-ai/bff/internal/api/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ func (app *App) Routes() http.Handler {
316316

317317
// AI Assets Models (Kubernetes)
318318
apiRouter.GET(constants.ModelsAAPath, app.AttachNamespace(app.RequireAccessToService(app.ModelsAAHandler)))
319+
apiRouter.POST(constants.ExternalModelsPath, app.AttachNamespace(app.RequireAccessToService(app.CreateExternalModelHandler)))
319320

320321
// Settings path namespace endpoints. This endpoint will get all the namespaces
321322
apiRouter.GET(constants.NamespacesPath, app.RequireAccessToService(app.GetNamespaceHandler))
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/julienschmidt/httprouter"
8+
"github.com/opendatahub-io/gen-ai/internal/constants"
9+
"github.com/opendatahub-io/gen-ai/internal/integrations"
10+
"github.com/opendatahub-io/gen-ai/internal/models"
11+
)
12+
13+
type CreateExternalModelEnvelope Envelope[models.AAModel, None]
14+
15+
// CreateExternalModelHandler handles the creation of external model endpoints
16+
func (app *App) CreateExternalModelHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
17+
ctx := r.Context()
18+
19+
// Get namespace from context
20+
namespace, ok := r.Context().Value(constants.NamespaceQueryParameterKey).(string)
21+
if !ok || namespace == "" {
22+
app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context"))
23+
return
24+
}
25+
26+
// Get the request identity from context
27+
identity, ok := ctx.Value(constants.RequestIdentityKey).(*integrations.RequestIdentity)
28+
if !ok || identity == nil {
29+
app.unauthorizedResponse(w, r, fmt.Errorf("missing RequestIdentity in context"))
30+
return
31+
}
32+
33+
// Parse request body
34+
var req models.ExternalModelRequest
35+
err := app.ReadJSON(w, r, &req)
36+
if err != nil {
37+
app.badRequestResponse(w, r, err)
38+
return
39+
}
40+
41+
// Validate required fields
42+
if req.ModelID == "" {
43+
app.badRequestResponse(w, r, fmt.Errorf("model_id is required"))
44+
return
45+
}
46+
if req.ModelDisplayName == "" {
47+
app.badRequestResponse(w, r, fmt.Errorf("model_display_name is required"))
48+
return
49+
}
50+
if req.BaseURL == "" {
51+
app.badRequestResponse(w, r, fmt.Errorf("base_url is required"))
52+
return
53+
}
54+
if req.SecretValue == "" {
55+
app.badRequestResponse(w, r, fmt.Errorf("secret_value is required"))
56+
return
57+
}
58+
if req.ProviderType == "" {
59+
app.badRequestResponse(w, r, fmt.Errorf("provider_type is required"))
60+
return
61+
}
62+
if req.ModelType == "" {
63+
app.badRequestResponse(w, r, fmt.Errorf("model_type is required"))
64+
return
65+
}
66+
67+
// Validate provider type
68+
validProviderTypes := map[models.ProviderTypeEnum]bool{
69+
models.ProviderTypeGemini: true,
70+
models.ProviderTypeOpenAI: true,
71+
models.ProviderTypeAnthropic: true,
72+
models.ProviderTypeVLLM: true,
73+
}
74+
if !validProviderTypes[req.ProviderType] {
75+
app.badRequestResponse(w, r, fmt.Errorf("invalid provider_type: %s", req.ProviderType))
76+
return
77+
}
78+
79+
// Validate model type
80+
validModelTypes := map[models.ModelTypeEnum]bool{
81+
models.ModelTypeEmbedding: true,
82+
models.ModelTypeLLM: true,
83+
}
84+
if !validModelTypes[req.ModelType] {
85+
app.badRequestResponse(w, r, fmt.Errorf("invalid model_type: %s", req.ModelType))
86+
return
87+
}
88+
89+
// Get Kubernetes client
90+
client, err := app.kubernetesClientFactory.GetClient(ctx)
91+
if err != nil {
92+
app.badRequestResponse(w, r, err)
93+
return
94+
}
95+
96+
// Create external model
97+
response, err := app.repositories.ExternalModels.CreateExternalModel(client, ctx, identity, namespace, req)
98+
if err != nil {
99+
app.serverErrorResponse(w, r, err)
100+
return
101+
}
102+
103+
// Return success response
104+
envelope := CreateExternalModelEnvelope{
105+
Data: *response,
106+
}
107+
err = app.WriteJSON(w, http.StatusCreated, envelope, nil)
108+
if err != nil {
109+
app.serverErrorResponse(w, r, err)
110+
return
111+
}
112+
}

0 commit comments

Comments
 (0)