mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
f9f968f13f
* feat(models): add per-model probe testing and auto-detect in UI Move health probes from provider level to model level for precise testing with real model_id and client_type. Provider test is now a simple reachability check. Backend: - Add POST /models/:id/test endpoint that probes the model's provider using its actual model_id and client_type - Add model healthcheck checker for bot health checks (chat/memory/embedding) - Simplify provider test to reachability-only Frontend: - Auto-probe models on mount with status indicator (green/yellow/red dot + latency) - Auto-probe provider reachability on load and on provider switch - Fix missing faBolt icon registration - Manual re-probe via refresh button Closes #117 * fix(models): increase probe timeout to 15s for slow providers Some providers (e.g. DashScope) exceed the 5s probe timeout, causing false-negative "context deadline exceeded" errors. Increase per-probe timeout to 15s and healthcheck overall timeout to 30s. * fix(sdk): regenerate exports after merge conflict Resolve duplicate SDK exports introduced by merge conflict resolution so the web build can compile again while preserving new model probe endpoints.
158 lines
3.8 KiB
Go
158 lines
3.8 KiB
Go
package models
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type ModelType string
|
|
|
|
const (
|
|
ModelTypeChat ModelType = "chat"
|
|
ModelTypeEmbedding ModelType = "embedding"
|
|
)
|
|
|
|
const (
|
|
ModelInputText = "text"
|
|
ModelInputImage = "image"
|
|
ModelInputAudio = "audio"
|
|
ModelInputVideo = "video"
|
|
ModelInputFile = "file"
|
|
)
|
|
|
|
type ClientType string
|
|
|
|
const (
|
|
ClientTypeOpenAIResponses ClientType = "openai-responses"
|
|
ClientTypeOpenAICompletions ClientType = "openai-completions"
|
|
ClientTypeAnthropicMessages ClientType = "anthropic-messages"
|
|
ClientTypeGoogleGenerativeAI ClientType = "google-generative-ai"
|
|
)
|
|
|
|
type Model struct {
|
|
ModelID string `json:"model_id"`
|
|
Name string `json:"name"`
|
|
LlmProviderID string `json:"llm_provider_id"`
|
|
ClientType ClientType `json:"client_type,omitempty"`
|
|
InputModalities []string `json:"input_modalities,omitempty"`
|
|
SupportsReasoning bool `json:"supports_reasoning"`
|
|
Type ModelType `json:"type"`
|
|
Dimensions int `json:"dimensions"`
|
|
}
|
|
|
|
// validInputModalities is the set of recognised input modality tokens.
|
|
var validInputModalities = map[string]struct{}{
|
|
ModelInputText: {}, ModelInputImage: {}, ModelInputAudio: {},
|
|
ModelInputVideo: {}, ModelInputFile: {},
|
|
}
|
|
|
|
func (m *Model) Validate() error {
|
|
if m.ModelID == "" {
|
|
return errors.New("model ID is required")
|
|
}
|
|
if m.LlmProviderID == "" {
|
|
return errors.New("llm provider ID is required")
|
|
}
|
|
if _, err := uuid.Parse(m.LlmProviderID); err != nil {
|
|
return errors.New("llm provider ID must be a valid UUID")
|
|
}
|
|
if m.Type != ModelTypeChat && m.Type != ModelTypeEmbedding {
|
|
return errors.New("invalid model type")
|
|
}
|
|
if m.Type == ModelTypeChat {
|
|
if m.ClientType == "" {
|
|
return errors.New("client_type is required for chat models")
|
|
}
|
|
if !isValidClientType(m.ClientType) {
|
|
return fmt.Errorf("invalid client_type: %s", m.ClientType)
|
|
}
|
|
}
|
|
if m.Type == ModelTypeEmbedding && m.Dimensions <= 0 {
|
|
return errors.New("dimensions must be greater than 0")
|
|
}
|
|
if m.Type == ModelTypeChat {
|
|
for _, mod := range m.InputModalities {
|
|
if _, ok := validInputModalities[mod]; !ok {
|
|
return fmt.Errorf("invalid input modality: %s", mod)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HasInputModality checks whether the model supports a given input modality.
|
|
func (m *Model) HasInputModality(mod string) bool {
|
|
for _, v := range m.InputModalities {
|
|
if v == mod {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsMultimodal returns true if the model supports any input modality beyond text.
|
|
func (m *Model) IsMultimodal() bool {
|
|
for _, v := range m.InputModalities {
|
|
if v != ModelInputText {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type AddRequest Model
|
|
|
|
type AddResponse struct {
|
|
ID string `json:"id"`
|
|
ModelID string `json:"model_id"`
|
|
}
|
|
|
|
type GetRequest struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
type GetResponse struct {
|
|
ID string `json:"id"`
|
|
ModelID string `json:"model_id"`
|
|
Model
|
|
}
|
|
|
|
type UpdateRequest Model
|
|
|
|
type ListRequest struct {
|
|
Type ModelType `json:"type,omitempty"`
|
|
ClientType ClientType `json:"client_type,omitempty"`
|
|
}
|
|
|
|
type DeleteRequest struct {
|
|
ID string `json:"id,omitempty"`
|
|
ModelID string `json:"model_id,omitempty"`
|
|
}
|
|
|
|
type DeleteResponse struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type CountResponse struct {
|
|
Count int64 `json:"count"`
|
|
}
|
|
|
|
// TestStatus represents the outcome of probing a model.
|
|
type TestStatus string
|
|
|
|
const (
|
|
TestStatusOK TestStatus = "ok"
|
|
TestStatusAuthError TestStatus = "auth_error"
|
|
TestStatusError TestStatus = "error"
|
|
)
|
|
|
|
// TestResponse is returned by POST /models/:id/test.
|
|
type TestResponse struct {
|
|
Status TestStatus `json:"status"`
|
|
Reachable bool `json:"reachable"`
|
|
LatencyMs int64 `json:"latency_ms,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|