iop/apps/edge/internal/openai/routes.go
toki 4c8441e6c9 feat(credential): Provider Credential Slot 라우팅을 구현한다
사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
2026-08-02 09:10:11 +09:00

147 lines
4.6 KiB
Go

package openai
import (
"net/http"
"strings"
"time"
"iop/apps/edge/internal/authprojection"
)
func (s *Server) routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", s.handleHealthz)
mux.HandleFunc("/v1/models", s.withAuth(s.handleModels))
mux.HandleFunc("/v1/chat/completions", s.withAuth(s.handleChatCompletions))
mux.HandleFunc("/v1/responses", s.withAuth(s.handleResponses))
s.registerAnthropicRoutes(mux)
mux.HandleFunc("/api/", s.withAuth(s.handleOllamaAPI))
return mux
}
func (s *Server) registerAnthropicRoutes(mux *http.ServeMux) {
for _, path := range []string{"/v1/messages", "/anthropic/v1/messages"} {
mux.HandleFunc(path, s.withAuth(s.handleAnthropicMessages))
}
for _, path := range []string{"/v1/messages/count_tokens", "/anthropic/v1/messages/count_tokens"} {
mux.HandleFunc(path, s.withAuth(s.handleAnthropicCountTokens))
}
mux.HandleFunc("/anthropic/v1/models", s.withAuth(s.handleModels))
}
func (s *Server) withAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
principal, view, ok := s.authenticatePrincipal(r)
if !ok {
s.writeAuthenticationFailure(w, r)
return
}
ctx := withPrincipal(r.Context(), principal)
if view.State == authprojection.StateFresh {
ctx = withAuthenticatedProjectionView(ctx, view)
}
if s.managedCredentialPlane() && hasLegacyProviderCredential(r, s.cfg.ProviderAuth.FromHeader) {
s.writeCallerProviderCredentialRejection(w, r)
return
}
next(w, r.WithContext(ctx))
}
}
func (s *Server) writeCallerProviderCredentialRejection(w http.ResponseWriter, r *http.Request) {
const message = "caller provider credentials are not allowed in managed credential mode"
if isAnthropicRequest(r) {
writeAnthropicError(w, http.StatusBadRequest, "invalid_request_error", message)
return
}
writeError(w, http.StatusBadRequest, "invalid_request_error", message)
}
func (s *Server) writeAuthenticationFailure(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", `Bearer realm="iop-openai"`)
if isAnthropicRequest(r) {
writeAnthropicError(w, http.StatusUnauthorized, "authentication_error", "authentication failed")
return
}
writeError(w, http.StatusUnauthorized, "unauthorized", "unauthorized")
}
func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (s *Server) handleModels(w http.ResponseWriter, r *http.Request) {
anthropic := strings.HasPrefix(r.URL.Path, "/anthropic/v1/") || strings.TrimSpace(r.Header.Get(anthropicVersionHeader)) != ""
if r.Method != http.MethodGet {
if anthropic {
writeAnthropicError(w, http.StatusMethodNotAllowed, "invalid_request_error", "method not allowed")
} else {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
}
return
}
models, err := s.advertisedModelsForPrincipal(r.Context())
if err != nil {
s.writeManagedRouteError(w, r, err)
return
}
if anthropic {
if err := validateAnthropicHeaders(r, false); err != nil {
writeAnthropicError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
return
}
writeAnthropicModels(w, models)
return
}
data := make([]openAIModel, 0, len(models))
for _, model := range models {
data = append(data, openAIModel{
ID: model.ID,
Object: "model",
Created: time.Now().Unix(),
OwnedBy: "iop",
})
}
writeJSON(w, http.StatusOK, openAIModelsResponse{Object: "list", Data: data})
}
type advertisedModel struct {
ID string
DisplayName string
}
func (s *Server) advertisedModels() []advertisedModel {
var models []advertisedModel
modelCatalog := s.modelCatalogSnapshot()
if len(modelCatalog) > 0 {
// Provider pool catalog takes priority over legacy model_routes.
for _, entry := range modelCatalog {
if id := strings.TrimSpace(entry.ID); id != "" {
displayName := strings.TrimSpace(entry.DisplayName)
if displayName == "" {
displayName = id
}
models = append(models, advertisedModel{ID: id, DisplayName: displayName})
}
}
} else if len(s.cfg.ModelRoutes) > 0 {
for _, route := range s.cfg.ModelRoutes {
id := strings.TrimSpace(route.Model)
if id != "" && route.Target != "" {
models = append(models, advertisedModel{ID: id, DisplayName: id})
}
}
} else {
modelIDs := s.cfg.Models
if len(modelIDs) == 0 && s.cfg.Target != "" {
modelIDs = []string{s.cfg.Target}
}
for _, id := range modelIDs {
id = strings.TrimSpace(id)
if id != "" {
models = append(models, advertisedModel{ID: id, DisplayName: id})
}
}
}
return models
}