mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-08-29 03:51:54 +08:00
feat(llm): add llm-router-agent (#25128)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/onecloud/cmd/climc/shell"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
modules "yunion.io/x/onecloud/pkg/mcclient/modules/llm"
|
||||
options "yunion.io/x/onecloud/pkg/mcclient/options/llm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmd := shell.NewResourceCmd(&modules.LLMRouterAgent)
|
||||
cmd.List(new(options.LLMRouterAgentListOptions))
|
||||
cmd.Show(new(options.LLMRouterAgentShowOptions))
|
||||
cmd.Create(new(options.LLMRouterAgentCreateOptions))
|
||||
cmd.Update(new(options.LLMRouterAgentUpdateOptions))
|
||||
cmd.Delete(new(options.LLMRouterAgentDeleteOptions))
|
||||
shell.R(&options.LLMRouterAgentRouteOptions{}, "llm-router-agent-route", "Route llm_router_agent", routeLLMRouterAgent)
|
||||
}
|
||||
|
||||
func routeLLMRouterAgent(s *mcclient.ClientSession, args *options.LLMRouterAgentRouteOptions) error {
|
||||
id := args.ID
|
||||
if id != "default" {
|
||||
var err error
|
||||
id, err = modules.LLMRouterAgent.GetId(s, args.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
bodyJSON, err := args.Params()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headers := http.Header{}
|
||||
headers.Set("Content-Type", "application/json")
|
||||
resp, err := s.RawVersionRequest(
|
||||
modules.LLMRouterAgent.ServiceType(),
|
||||
modules.LLMRouterAgent.EndpointType(),
|
||||
"POST",
|
||||
fmt.Sprintf("/llm_router_agents/%s/route", id),
|
||||
headers,
|
||||
strings.NewReader(bodyJSON.String()),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("Error: %s %s", resp.Status, string(respBody))
|
||||
}
|
||||
obj, err := jsonutils.Parse(respBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shell.PrintObject(obj)
|
||||
return nil
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/pkg/errors"
|
||||
"yunion.io/x/pkg/tristate"
|
||||
"yunion.io/x/pkg/util/httputils"
|
||||
"yunion.io/x/sqlchemy"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/aiproxy"
|
||||
@@ -629,7 +630,7 @@ func callAiRoutingRouter(ctx context.Context, routing *SAiRouting, routerModel s
|
||||
return nil, errors.Wrap(err, "new router request")
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := httputils.GetTimeoutClient(time.Duration(timeout) * time.Second).Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "call ai routing router")
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ const (
|
||||
LLM_IMAGE_TYPE_COMFYUI LLMImageType = "comfyui"
|
||||
LLM_IMAGE_TYPE_OPENCLAW LLMImageType = "openclaw"
|
||||
LLM_IMAGE_TYPE_HERMES_AGENT LLMImageType = "hermes-agent"
|
||||
LLM_IMAGE_TYPE_LLM_ROUTER LLMImageType = "llm-router"
|
||||
LLM_IMAGE_TYPE_DESKTOP LLMImageType = "desktop"
|
||||
LLM_IMAGE_TYPE_BENCHMARK LLMImageType = "benchmark"
|
||||
)
|
||||
@@ -30,7 +29,6 @@ var (
|
||||
string(LLM_IMAGE_TYPE_COMFYUI),
|
||||
string(LLM_IMAGE_TYPE_OPENCLAW),
|
||||
string(LLM_IMAGE_TYPE_HERMES_AGENT),
|
||||
string(LLM_IMAGE_TYPE_LLM_ROUTER),
|
||||
string(LLM_IMAGE_TYPE_DESKTOP),
|
||||
string(LLM_IMAGE_TYPE_BENCHMARK),
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ const (
|
||||
STATUS_READY = "ready"
|
||||
)
|
||||
|
||||
const LLM_DEFAULT_CONTEXT_TOKENS = 8192
|
||||
|
||||
const (
|
||||
/* 未知 */
|
||||
LLM_STATUS_UNKNOWN = "unknown"
|
||||
|
||||
@@ -16,7 +16,6 @@ const (
|
||||
LLM_CONTAINER_COMFYUI LLMContainerType = "comfyui"
|
||||
LLM_CONTAINER_OPENCLAW LLMContainerType = "openclaw"
|
||||
LLM_CONTAINER_HERMES_AGENT LLMContainerType = "hermes-agent"
|
||||
LLM_CONTAINER_LLM_ROUTER LLMContainerType = "llm-router"
|
||||
LLM_CONTAINER_DESKTOP LLMContainerType = "desktop"
|
||||
)
|
||||
|
||||
@@ -29,7 +28,6 @@ var (
|
||||
string(LLM_CONTAINER_COMFYUI),
|
||||
string(LLM_CONTAINER_OPENCLAW),
|
||||
string(LLM_CONTAINER_HERMES_AGENT),
|
||||
string(LLM_CONTAINER_LLM_ROUTER),
|
||||
string(LLM_CONTAINER_DESKTOP),
|
||||
)
|
||||
LLM_INSTANT_MODEL_TYPES = sets.NewString(
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package llm
|
||||
|
||||
const (
|
||||
LLM_ROUTER = "llm-router"
|
||||
LLM_ROUTER_EXEC_PATH = "llm-router-runtime"
|
||||
LLM_ROUTER_DEFAULT_RUNTIME = "llmrouter-lib"
|
||||
LLM_ROUTER_DEFAULT_MODEL_DIR = "/models"
|
||||
LLM_ROUTER_DEFAULT_ROUTE_PATH = "/v1/route"
|
||||
LLM_ROUTER_DEFAULT_HEALTH_PATH = "/health"
|
||||
LLM_ROUTER_DEFAULT_PORT = 8000
|
||||
LLM_ROUTER_CACHE_DIR = "/root/.cache"
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
package llm
|
||||
|
||||
import "yunion.io/x/onecloud/pkg/apis"
|
||||
|
||||
const (
|
||||
LLM_ROUTER_ROUTE_SIMPLE = "simple"
|
||||
LLM_ROUTER_ROUTE_COMPLEX = "complex"
|
||||
|
||||
LLM_ROUTER_DEFAULT_ROUTE = LLM_ROUTER_ROUTE_COMPLEX
|
||||
LLM_ROUTER_DEFAULT_MAX_PROMPT_CHARS = LLM_DEFAULT_CONTEXT_TOKENS * 3 / 4
|
||||
LLM_ROUTER_DEFAULT_MAX_DECISION_TOKENS = 64
|
||||
LLM_ROUTER_DEFAULT_SIMPLE_DEFINITION = "适合翻译、摘要、短问答和格式转换。"
|
||||
LLM_ROUTER_DEFAULT_COMPLEX_DEFINITION = "适合代码、数学、长上下文、复杂推理和日志分析。"
|
||||
)
|
||||
|
||||
type LLMRouterAgentListInput struct {
|
||||
apis.SharableVirtualResourceListInput
|
||||
|
||||
LLMDriver string `json:"llm_driver"`
|
||||
}
|
||||
|
||||
type LLMRouterAgentCreateInput struct {
|
||||
apis.SharableVirtualResourceCreateInput
|
||||
|
||||
LLMId string `json:"llm_id" help:"LLM instance ID; if set, llm_url is resolved from it"`
|
||||
LLMUrl string `json:"llm_url" help:"decision model OpenAI-compatible base URL"`
|
||||
LLMDriver string `json:"llm_driver" help:"decision model driver, default openai"`
|
||||
Model string `json:"model" help:"decision model name"`
|
||||
ApiKey string `json:"api_key" help:"decision model API key"`
|
||||
DefaultRoute string `json:"default_route" help:"fallback route, default complex"`
|
||||
MaxPromptChars int `json:"max_prompt_chars" help:"request char limit; 0 auto-detects /v1/models context, longer requests use complex"`
|
||||
MaxDecisionTokens int `json:"max_decision_tokens" help:"decision answer token hint"`
|
||||
CandidateMapping map[string][]string `json:"candidate_mapping" help:"route to candidate model names"`
|
||||
SimpleDefinition string `json:"simple_definition" help:"definition of simple requests"`
|
||||
ComplexDefinition string `json:"complex_definition" help:"definition of complex requests"`
|
||||
SimpleExamples []string `json:"simple_examples" help:"examples routed to simple"`
|
||||
ComplexExamples []string `json:"complex_examples" help:"examples routed to complex"`
|
||||
}
|
||||
|
||||
type LLMRouterAgentUpdateInput struct {
|
||||
apis.SharableVirtualResourceBaseUpdateInput
|
||||
|
||||
LLMId *string `json:"llm_id,omitempty" help:"LLM instance ID; if set, llm_url is resolved from it"`
|
||||
LLMUrl *string `json:"llm_url,omitempty" help:"decision model OpenAI-compatible base URL"`
|
||||
LLMDriver *string `json:"llm_driver,omitempty" help:"decision model driver"`
|
||||
Model *string `json:"model,omitempty" help:"decision model name"`
|
||||
ApiKey *string `json:"api_key,omitempty" help:"decision model API key"`
|
||||
DefaultRoute *string `json:"default_route,omitempty" help:"fallback route"`
|
||||
MaxPromptChars *int `json:"max_prompt_chars,omitempty" help:"request char limit; 0 recomputes from /v1/models context"`
|
||||
MaxDecisionTokens *int `json:"max_decision_tokens,omitempty" help:"decision answer token hint"`
|
||||
CandidateMapping *map[string][]string `json:"candidate_mapping,omitempty" help:"route to candidate model names"`
|
||||
SimpleDefinition *string `json:"simple_definition,omitempty" help:"definition of simple requests"`
|
||||
ComplexDefinition *string `json:"complex_definition,omitempty" help:"definition of complex requests"`
|
||||
SimpleExamples *[]string `json:"simple_examples,omitempty" help:"replace examples routed to simple"`
|
||||
ComplexExamples *[]string `json:"complex_examples,omitempty" help:"replace examples routed to complex"`
|
||||
}
|
||||
|
||||
type LLMRouterAgentDetails struct {
|
||||
apis.SharableVirtualResourceDetails
|
||||
|
||||
LLMId string `json:"llm_id"`
|
||||
LLMName string `json:"llm_name"`
|
||||
}
|
||||
|
||||
type LLMRouterPromptConfig struct {
|
||||
SimpleDefinition string `json:"simple_definition"`
|
||||
ComplexDefinition string `json:"complex_definition"`
|
||||
SimpleExamples []string `json:"simple_examples"`
|
||||
ComplexExamples []string `json:"complex_examples"`
|
||||
}
|
||||
|
||||
type LLMRouterExpandExamplesInput struct {
|
||||
SimpleDefinition *string `json:"simple_definition,omitempty"`
|
||||
ComplexDefinition *string `json:"complex_definition,omitempty"`
|
||||
SimpleExamples *[]string `json:"simple_examples,omitempty"`
|
||||
ComplexExamples *[]string `json:"complex_examples,omitempty"`
|
||||
}
|
||||
|
||||
type LLMRouterMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content interface{} `json:"content"`
|
||||
}
|
||||
|
||||
type LLMRouterRouteRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []LLMRouterMessage `json:"messages"`
|
||||
Candidates []string `json:"candidates"`
|
||||
}
|
||||
|
||||
type LLMRouterRouteResponse struct {
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
type LLMRouterDecision struct {
|
||||
Route string `json:"route"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
@@ -30,7 +30,6 @@ type LLMSpec struct {
|
||||
ComfyUI *LLMSpecComfyUI `json:"comfyui,omitempty"`
|
||||
OpenClaw *LLMSpecOpenClaw `json:"openclaw,omitempty"`
|
||||
HermesAgent *LLMSpecHermesAgent `json:"hermes_agent,omitempty"`
|
||||
LLMRouter *LLMSpecLLMRouter `json:"llm_router,omitempty"`
|
||||
}
|
||||
|
||||
func (s *LLMSpec) String() string {
|
||||
@@ -41,7 +40,7 @@ func (s *LLMSpec) IsZero() bool {
|
||||
if s == nil {
|
||||
return true
|
||||
}
|
||||
return s.Ollama == nil && s.Vllm == nil && s.SGLang == nil && s.Dify == nil && s.ComfyUI == nil && s.OpenClaw == nil && s.HermesAgent == nil && s.LLMRouter == nil
|
||||
return s.Ollama == nil && s.Vllm == nil && s.SGLang == nil && s.Dify == nil && s.ComfyUI == nil && s.OpenClaw == nil && s.HermesAgent == nil
|
||||
}
|
||||
|
||||
// LLMSpecOllama holds type-specific fields for ollama SKUs.
|
||||
@@ -196,43 +195,6 @@ func (s *LLMSpecHermesAgent) IsZero() bool {
|
||||
return s.LLMId == "" && s.LLMUrl == "" && s.Model == "" && s.ApiKey == "" && s.ContextLength == 0
|
||||
}
|
||||
|
||||
type LLMRouterEnv struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type LLMRouterArg struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type LLMSpecLLMRouter struct {
|
||||
Runtime string `json:"runtime"`
|
||||
RouterMethod string `json:"router_method"`
|
||||
ConfigPath string `json:"config_path,omitempty"`
|
||||
ModelDir string `json:"model_dir,omitempty"`
|
||||
RoutePath string `json:"route_path,omitempty"`
|
||||
HealthPath string `json:"health_path,omitempty"`
|
||||
CandidateMappingPath string `json:"candidate_mapping_path,omitempty"`
|
||||
CustomizedEnvs []*LLMRouterEnv `json:"customized_envs,omitempty"`
|
||||
CustomizedArgs []*LLMRouterArg `json:"customized_args,omitempty"`
|
||||
Extra map[string]interface{} `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
func (s *LLMSpecLLMRouter) String() string {
|
||||
return jsonutils.Marshal(s).String()
|
||||
}
|
||||
|
||||
func (s *LLMSpecLLMRouter) IsZero() bool {
|
||||
if s == nil {
|
||||
return true
|
||||
}
|
||||
return s.Runtime == "" && s.RouterMethod == "" && s.ConfigPath == "" &&
|
||||
s.ModelDir == "" && s.RoutePath == "" && s.HealthPath == "" &&
|
||||
s.CandidateMappingPath == "" && len(s.CustomizedEnvs) == 0 &&
|
||||
len(s.CustomizedArgs) == 0 && len(s.Extra) == 0
|
||||
}
|
||||
|
||||
func init() {
|
||||
gotypes.RegisterSerializable(reflect.TypeOf(new(LLMSpec)), func() gotypes.ISerializable {
|
||||
return new(LLMSpec)
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
package llm_container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/pkg/errors"
|
||||
|
||||
commonapi "yunion.io/x/onecloud/pkg/apis"
|
||||
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/llm/models"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
)
|
||||
|
||||
func init() {
|
||||
models.RegisterLLMContainerDriver(newLLMRouter())
|
||||
}
|
||||
|
||||
type llmRouter struct {
|
||||
baseDriver
|
||||
}
|
||||
|
||||
func newLLMRouter() models.ILLMContainerDriver {
|
||||
return &llmRouter{baseDriver: newBaseDriver(api.LLM_CONTAINER_LLM_ROUTER)}
|
||||
}
|
||||
|
||||
func (r *llmRouter) GetSpec(sku *models.SLLMSku) interface{} {
|
||||
if sku == nil || sku.LLMType != string(api.LLM_CONTAINER_LLM_ROUTER) || sku.LLMSpec == nil || sku.LLMSpec.LLMRouter == nil {
|
||||
return nil
|
||||
}
|
||||
return sku.LLMSpec.LLMRouter
|
||||
}
|
||||
|
||||
func copyLLMRouterSpec(in *api.LLMSpecLLMRouter) *api.LLMSpecLLMRouter {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := *in
|
||||
out.Runtime = strings.TrimSpace(out.Runtime)
|
||||
out.RouterMethod = strings.TrimSpace(out.RouterMethod)
|
||||
out.ConfigPath = strings.TrimSpace(out.ConfigPath)
|
||||
out.ModelDir = strings.TrimSpace(out.ModelDir)
|
||||
out.RoutePath = strings.TrimSpace(out.RoutePath)
|
||||
out.HealthPath = strings.TrimSpace(out.HealthPath)
|
||||
out.CandidateMappingPath = strings.TrimSpace(out.CandidateMappingPath)
|
||||
if in.CustomizedEnvs != nil {
|
||||
out.CustomizedEnvs = append([]*api.LLMRouterEnv(nil), in.CustomizedEnvs...)
|
||||
}
|
||||
if in.CustomizedArgs != nil {
|
||||
out.CustomizedArgs = append([]*api.LLMRouterArg(nil), in.CustomizedArgs...)
|
||||
}
|
||||
if in.Extra != nil {
|
||||
out.Extra = map[string]interface{}{}
|
||||
for k, v := range in.Extra {
|
||||
out.Extra[k] = v
|
||||
}
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
func mergeLLMRouterSpecs(base, override *api.LLMSpecLLMRouter) *api.LLMSpecLLMRouter {
|
||||
if base == nil && override == nil {
|
||||
return nil
|
||||
}
|
||||
out := copyLLMRouterSpec(base)
|
||||
if out == nil {
|
||||
out = &api.LLMSpecLLMRouter{}
|
||||
}
|
||||
ov := copyLLMRouterSpec(override)
|
||||
if ov == nil {
|
||||
return out
|
||||
}
|
||||
if ov.Runtime != "" {
|
||||
out.Runtime = ov.Runtime
|
||||
}
|
||||
if ov.RouterMethod != "" {
|
||||
out.RouterMethod = ov.RouterMethod
|
||||
}
|
||||
if ov.ConfigPath != "" {
|
||||
out.ConfigPath = ov.ConfigPath
|
||||
}
|
||||
if ov.ModelDir != "" {
|
||||
out.ModelDir = ov.ModelDir
|
||||
}
|
||||
if ov.RoutePath != "" {
|
||||
out.RoutePath = ov.RoutePath
|
||||
}
|
||||
if ov.HealthPath != "" {
|
||||
out.HealthPath = ov.HealthPath
|
||||
}
|
||||
if ov.CandidateMappingPath != "" {
|
||||
out.CandidateMappingPath = ov.CandidateMappingPath
|
||||
}
|
||||
if ov.CustomizedEnvs != nil {
|
||||
out.CustomizedEnvs = ov.CustomizedEnvs
|
||||
}
|
||||
if ov.CustomizedArgs != nil {
|
||||
out.CustomizedArgs = ov.CustomizedArgs
|
||||
}
|
||||
if ov.Extra != nil {
|
||||
if out.Extra == nil {
|
||||
out.Extra = map[string]interface{}{}
|
||||
}
|
||||
for k, v := range ov.Extra {
|
||||
out.Extra[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *llmRouter) normalizeSpec(in *api.LLMSpecLLMRouter) (*api.LLMSpecLLMRouter, error) {
|
||||
out := copyLLMRouterSpec(in)
|
||||
if out == nil {
|
||||
out = &api.LLMSpecLLMRouter{}
|
||||
}
|
||||
out.Runtime = api.LLM_ROUTER_DEFAULT_RUNTIME
|
||||
if out.ModelDir == "" {
|
||||
out.ModelDir = api.LLM_ROUTER_DEFAULT_MODEL_DIR
|
||||
}
|
||||
if out.RoutePath == "" {
|
||||
out.RoutePath = api.LLM_ROUTER_DEFAULT_ROUTE_PATH
|
||||
}
|
||||
if out.HealthPath == "" {
|
||||
out.HealthPath = api.LLM_ROUTER_DEFAULT_HEALTH_PATH
|
||||
}
|
||||
if out.RouterMethod == "" {
|
||||
return out, errors.Wrap(httperrors.ErrMissingParameter, "llm_router.router_method is required")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *llmRouter) GetEffectiveSpec(llm *models.SLLM, sku *models.SLLMSku) interface{} {
|
||||
var skuSpec *api.LLMSpecLLMRouter
|
||||
if spec := r.GetSpec(sku); spec != nil {
|
||||
skuSpec = spec.(*api.LLMSpecLLMRouter)
|
||||
}
|
||||
var llmSpec *api.LLMSpecLLMRouter
|
||||
if llm != nil && llm.LLMSpec != nil && llm.LLMSpec.LLMRouter != nil {
|
||||
llmSpec = llm.LLMSpec.LLMRouter
|
||||
}
|
||||
out, err := r.normalizeSpec(mergeLLMRouterSpecs(skuSpec, llmSpec))
|
||||
if err != nil {
|
||||
return mergeLLMRouterSpecs(skuSpec, llmSpec)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *llmRouter) ValidateLLMSkuCreateData(ctx context.Context, userCred mcclient.TokenCredential, input *api.LLMSkuCreateInput) (*api.LLMSkuCreateInput, error) {
|
||||
var err error
|
||||
input, err = r.baseDriver.ValidateLLMSkuCreateData(ctx, userCred, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec, err := r.ValidateLLMCreateSpec(ctx, userCred, nil, input.LLMSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.LLMSpec = spec
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func (r *llmRouter) ValidateLLMSkuUpdateData(ctx context.Context, userCred mcclient.TokenCredential, sku *models.SLLMSku, input *api.LLMSkuUpdateInput) (*api.LLMSkuUpdateInput, error) {
|
||||
var err error
|
||||
input, err = r.baseDriver.ValidateLLMSkuUpdateData(ctx, userCred, sku, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.LLMSpec != nil {
|
||||
input.LLMSpec, err = r.ValidateLLMUpdateSpec(ctx, userCred, nil, input.LLMSpec)
|
||||
}
|
||||
return input, err
|
||||
}
|
||||
|
||||
func (r *llmRouter) ValidateLLMCreateSpec(ctx context.Context, userCred mcclient.TokenCredential, sku *models.SLLMSku, input *api.LLMSpec) (*api.LLMSpec, error) {
|
||||
var skuSpec *api.LLMSpecLLMRouter
|
||||
if sku != nil && sku.LLMSpec != nil {
|
||||
skuSpec = sku.LLMSpec.LLMRouter
|
||||
}
|
||||
var inputSpec *api.LLMSpecLLMRouter
|
||||
if input != nil {
|
||||
inputSpec = input.LLMRouter
|
||||
}
|
||||
spec, err := r.normalizeSpec(mergeLLMRouterSpecs(skuSpec, inputSpec))
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
return &api.LLMSpec{LLMRouter: spec}, nil
|
||||
}
|
||||
|
||||
func (r *llmRouter) ValidateLLMUpdateSpec(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, input *api.LLMSpec) (*api.LLMSpec, error) {
|
||||
if input == nil || input.LLMRouter == nil {
|
||||
return input, nil
|
||||
}
|
||||
spec, err := r.normalizeSpec(input.LLMRouter)
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
return &api.LLMSpec{LLMRouter: spec}, nil
|
||||
}
|
||||
|
||||
type llmRouterRuntimeArg struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (a llmRouterRuntimeArg) String() string {
|
||||
key := strings.TrimSpace(a.Key)
|
||||
if key == "" {
|
||||
return ""
|
||||
}
|
||||
key = strings.TrimPrefix(key, "--")
|
||||
if strings.TrimSpace(a.Value) == "" {
|
||||
return "--" + key
|
||||
}
|
||||
return fmt.Sprintf("--%s %s", key, strings.TrimSpace(a.Value))
|
||||
}
|
||||
|
||||
func llmRouterCustomizedArgsToRuntime(args []*api.LLMRouterArg) []llmRouterRuntimeArg {
|
||||
out := make([]llmRouterRuntimeArg, 0, len(args))
|
||||
for _, arg := range args {
|
||||
if arg == nil || strings.TrimSpace(arg.Key) == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, llmRouterRuntimeArg{Key: arg.Key, Value: arg.Value})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildLLMRouterEntrypointScript(spec *api.LLMSpecLLMRouter) string {
|
||||
parts := []string{
|
||||
api.LLM_ROUTER_EXEC_PATH,
|
||||
"--host 0.0.0.0",
|
||||
fmt.Sprintf("--port %d", api.LLM_ROUTER_DEFAULT_PORT),
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func (r *llmRouter) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *models.SLLMImage, sku *models.SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) *computeapi.PodContainerCreateInput {
|
||||
effSpec := &api.LLMSpecLLMRouter{RouterMethod: "routerdc"}
|
||||
if eff := r.GetEffectiveSpec(llm, sku); eff != nil {
|
||||
effSpec = eff.(*api.LLMSpecLLMRouter)
|
||||
}
|
||||
envs := []*commonapi.ContainerKeyValue{
|
||||
models.NewEnv("LLM_ROUTER_RUNTIME", effSpec.Runtime),
|
||||
models.NewEnv("LLM_ROUTER_METHOD", effSpec.RouterMethod),
|
||||
models.NewEnv("LLM_ROUTER_MODEL_DIR", effSpec.ModelDir),
|
||||
models.NewEnv("LLM_ROUTER_ROUTE_PATH", effSpec.RoutePath),
|
||||
models.NewEnv("LLM_ROUTER_HEALTH_PATH", effSpec.HealthPath),
|
||||
}
|
||||
if effSpec.ConfigPath != "" {
|
||||
envs = append(envs, models.NewEnv("LLM_ROUTER_CONFIG", effSpec.ConfigPath))
|
||||
}
|
||||
if effSpec.CandidateMappingPath != "" {
|
||||
envs = append(envs, models.NewEnv("LLM_ROUTER_CANDIDATE_MAPPING", effSpec.CandidateMappingPath))
|
||||
}
|
||||
for _, env := range effSpec.CustomizedEnvs {
|
||||
if env != nil && strings.TrimSpace(env.Key) != "" {
|
||||
envs = append(envs, models.NewEnv(strings.TrimSpace(env.Key), env.Value))
|
||||
}
|
||||
}
|
||||
spec := computeapi.ContainerSpec{
|
||||
ContainerSpec: commonapi.ContainerSpec{
|
||||
Image: image.ToContainerImage(),
|
||||
ImageCredentialId: image.CredentialId,
|
||||
Command: []string{"/bin/sh", "-c"},
|
||||
Args: []string{buildLLMRouterEntrypointScript(effSpec)},
|
||||
EnableLxcfs: true,
|
||||
AlwaysRestart: true,
|
||||
Envs: envs,
|
||||
},
|
||||
}
|
||||
appendContainerIsolatedDevices(&spec, llm, sku, devices)
|
||||
diskIndex := 0
|
||||
spec.VolumeMounts = append(spec.VolumeMounts,
|
||||
&commonapi.ContainerVolumeMount{
|
||||
Disk: &commonapi.ContainerVolumeMountDisk{
|
||||
SubDirectory: api.LLM_ROUTER,
|
||||
Index: &diskIndex,
|
||||
},
|
||||
Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
|
||||
MountPath: effSpec.ModelDir,
|
||||
ReadOnly: false,
|
||||
},
|
||||
&commonapi.ContainerVolumeMount{
|
||||
Disk: &commonapi.ContainerVolumeMountDisk{
|
||||
SubDirectory: "cache",
|
||||
Index: &diskIndex,
|
||||
},
|
||||
Type: commonapi.CONTAINER_VOLUME_MOUNT_TYPE_DISK,
|
||||
MountPath: api.LLM_ROUTER_CACHE_DIR,
|
||||
ReadOnly: false,
|
||||
},
|
||||
)
|
||||
return &computeapi.PodContainerCreateInput{ContainerSpec: spec}
|
||||
}
|
||||
|
||||
func (r *llmRouter) GetContainerSpecs(ctx context.Context, llm *models.SLLM, image *models.SLLMImage, sku *models.SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) []*computeapi.PodContainerCreateInput {
|
||||
return []*computeapi.PodContainerCreateInput{
|
||||
r.GetContainerSpec(ctx, llm, image, sku, props, devices, diskId),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *llmRouter) GetLLMAccessUrlInfo(ctx context.Context, userCred mcclient.TokenCredential, llm *models.SLLM, input *models.LLMAccessInfoInput) (*api.LLMAccessUrlInfo, error) {
|
||||
return models.GetLLMAccessUrlInfo(ctx, userCred, llm, input, "http", api.LLM_ROUTER_DEFAULT_PORT)
|
||||
}
|
||||
@@ -914,6 +914,13 @@ func (llm *SLLM) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSO
|
||||
if cnt > 0 {
|
||||
return httperrors.NewConflictError("LLM is being used by %d MCPAgents", cnt)
|
||||
}
|
||||
cnt, err = GetLLMRouterAgentManager().Query().Equals("llm_id", llm.Id).CountWithError()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "GetLLMRouterAgentManager().Query().CountWithError")
|
||||
}
|
||||
if cnt > 0 {
|
||||
return httperrors.NewConflictError("LLM is being used by %d LLMRouterAgents", cnt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ func getDriver[K ~string, D any](drvs *drivers, typ K) D {
|
||||
func getDriverWithError[K ~string, D any](drvs *drivers, typ K) (D, error) {
|
||||
drv, err := drvs.GetWithError(string(typ))
|
||||
if err != nil {
|
||||
return drv.(D), err
|
||||
var zero D
|
||||
return zero, err
|
||||
}
|
||||
return drv.(D), nil
|
||||
}
|
||||
|
||||
@@ -23,11 +23,6 @@ const (
|
||||
autoGpuMemoryUtilizationMin = 0.05
|
||||
autoGpuMemoryUtilizationMax = 0.95
|
||||
|
||||
// vLLM derives an unset max-model-len from model config. When auto GPU
|
||||
// memory utilization is enabled, inject a conservative cap so the heuristic
|
||||
// VRAM estimate is not paired with an unexpectedly large context.
|
||||
autoGpuMemoryUtilizationDefaultContextTokens = int64(8192)
|
||||
|
||||
sglangAutoGpuMemoryMetadataReserveMB = 512
|
||||
)
|
||||
|
||||
@@ -465,7 +460,7 @@ func buildAutoGpuMemoryUtilizationLLMSpec(sku *SLLMSku, utilization float64) (*a
|
||||
if spec != nil && spec.Vllm != nil && !runtimeHasExplicitTokenLimit(sku) {
|
||||
spec.Vllm.CustomizedArgs = append(spec.Vllm.CustomizedArgs, &api.VllmCustomizedArg{
|
||||
Key: "max-model-len",
|
||||
Value: strconv.FormatInt(autoGpuMemoryUtilizationDefaultContextTokens, 10),
|
||||
Value: strconv.FormatInt(api.LLM_DEFAULT_CONTEXT_TOKENS, 10),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,825 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/errors"
|
||||
seclib "yunion.io/x/pkg/utils"
|
||||
"yunion.io/x/sqlchemy"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/llm/options"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/util/stringutils2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
GetLLMRouterAgentManager()
|
||||
}
|
||||
|
||||
var llmRouterAgentManager *SLLMRouterAgentManager
|
||||
|
||||
func GetLLMRouterAgentManager() *SLLMRouterAgentManager {
|
||||
if llmRouterAgentManager != nil {
|
||||
return llmRouterAgentManager
|
||||
}
|
||||
llmRouterAgentManager = &SLLMRouterAgentManager{
|
||||
SSharableVirtualResourceBaseManager: db.NewSharableVirtualResourceBaseManager(
|
||||
SLLMRouterAgent{},
|
||||
"llm_router_agents_tbl",
|
||||
"llm_router_agent",
|
||||
"llm_router_agents",
|
||||
),
|
||||
}
|
||||
llmRouterAgentManager.SetVirtualObject(llmRouterAgentManager)
|
||||
return llmRouterAgentManager
|
||||
}
|
||||
|
||||
type SLLMRouterAgentManager struct {
|
||||
db.SSharableVirtualResourceBaseManager
|
||||
}
|
||||
|
||||
type SLLMRouterAgent struct {
|
||||
db.SSharableVirtualResourceBase
|
||||
|
||||
LLMId string `width:"128" charset:"ascii" nullable:"true" list:"user" create:"optional" update:"user"`
|
||||
LLMUrl string `width:"512" charset:"utf8" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
LLMDriver string `width:"64" charset:"ascii" nullable:"false" list:"user" create:"optional" update:"user"`
|
||||
Model string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" update:"user"`
|
||||
ApiKey string `width:"512" charset:"utf8" nullable:"true" create:"optional" update:"user"`
|
||||
DefaultRoute string `width:"32" charset:"ascii" nullable:"false" default:"complex" list:"user" create:"optional" update:"user"`
|
||||
MaxPromptChars int `nullable:"false" default:"6144" list:"user" create:"optional" update:"user"`
|
||||
MaxDecisionTokens int `nullable:"false" default:"64" list:"user" create:"optional" update:"user"`
|
||||
CandidateMapping jsonutils.JSONObject `length:"long" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
|
||||
SimpleDefinition string `length:"long" charset:"utf8" nullable:"false" list:"user" create:"optional" update:"user"`
|
||||
ComplexDefinition string `length:"long" charset:"utf8" nullable:"false" list:"user" create:"optional" update:"user"`
|
||||
SimpleExamples jsonutils.JSONObject `length:"long" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
|
||||
ComplexExamples jsonutils.JSONObject `length:"long" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) BeforeInsert() {
|
||||
if len(agent.Id) == 0 {
|
||||
agent.Id = db.DefaultUUIDGenerator()
|
||||
}
|
||||
agent.encryptApiKey()
|
||||
agent.SSharableVirtualResourceBase.BeforeInsert()
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) BeforeUpdate() {
|
||||
agent.encryptApiKey()
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) encryptApiKey() {
|
||||
if len(agent.ApiKey) == 0 {
|
||||
return
|
||||
}
|
||||
if _, err := seclib.DescryptAESBase64(agent.Id, agent.ApiKey); err == nil {
|
||||
return
|
||||
}
|
||||
sec, err := seclib.EncryptAESBase64(agent.Id, agent.ApiKey)
|
||||
if err != nil {
|
||||
log.Errorf("EncryptAESBase64 fail %s", err)
|
||||
return
|
||||
}
|
||||
agent.ApiKey = sec
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) GetApiKey() (string, error) {
|
||||
if len(agent.ApiKey) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
key, err := seclib.DescryptAESBase64(agent.Id, agent.ApiKey)
|
||||
if err == nil {
|
||||
return key, nil
|
||||
}
|
||||
return "", errors.Wrap(err, "decrypt llm router api key")
|
||||
}
|
||||
|
||||
func (man *SLLMRouterAgentManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input *api.LLMRouterAgentCreateInput) (*api.LLMRouterAgentCreateInput, error) {
|
||||
var err error
|
||||
input.SharableVirtualResourceCreateInput, err = man.SSharableVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.SharableVirtualResourceCreateInput)
|
||||
if err != nil {
|
||||
return input, errors.Wrap(err, "validate SharableVirtualResourceCreateInput")
|
||||
}
|
||||
|
||||
if err := resolveRouterAgentLLM(ctx, userCred, query, &input.LLMId, &input.LLMUrl, &input.Model); err != nil {
|
||||
return input, err
|
||||
}
|
||||
input.LLMDriver = normalizeRouterLLMDriver(input.LLMDriver)
|
||||
if !api.IsLLMClientType(input.LLMDriver) {
|
||||
return input, errors.Wrapf(httperrors.ErrInputParameter, "llm_driver must be one of: %s, got: %s", api.LLM_CLIENT_TYPES.List(), input.LLMDriver)
|
||||
}
|
||||
input.LLMUrl = strings.TrimSpace(input.LLMUrl)
|
||||
input.Model = strings.TrimSpace(input.Model)
|
||||
if input.LLMUrl == "" {
|
||||
return input, errors.Wrap(httperrors.ErrInputParameter, "llm_url is required (or provide llm_id to auto-fetch)")
|
||||
}
|
||||
if input.Model == "" {
|
||||
return input, errors.Wrap(httperrors.ErrInputParameter, "model is required")
|
||||
}
|
||||
input.DefaultRoute = normalizeRouterRoute(input.DefaultRoute)
|
||||
input.MaxPromptChars, err = resolveRouterMaxPromptChars(ctx, input.MaxPromptChars, input.LLMUrl, input.Model, input.ApiKey)
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
input.MaxDecisionTokens = normalizePositiveInt(input.MaxDecisionTokens, api.LLM_ROUTER_DEFAULT_MAX_DECISION_TOKENS)
|
||||
input.CandidateMapping = normalizeCandidateMapping(input.CandidateMapping)
|
||||
promptConfig := normalizeRouterPromptConfig(api.LLMRouterPromptConfig{
|
||||
SimpleDefinition: input.SimpleDefinition,
|
||||
ComplexDefinition: input.ComplexDefinition,
|
||||
SimpleExamples: input.SimpleExamples,
|
||||
ComplexExamples: input.ComplexExamples,
|
||||
})
|
||||
input.SimpleDefinition = promptConfig.SimpleDefinition
|
||||
input.ComplexDefinition = promptConfig.ComplexDefinition
|
||||
input.SimpleExamples = promptConfig.SimpleExamples
|
||||
input.ComplexExamples = promptConfig.ComplexExamples
|
||||
input.Status = api.STATUS_READY
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.LLMRouterAgentUpdateInput) (api.LLMRouterAgentUpdateInput, error) {
|
||||
var err error
|
||||
input.SharableVirtualResourceBaseUpdateInput, err = agent.SSharableVirtualResourceBase.ValidateUpdateData(ctx, userCred, query, input.SharableVirtualResourceBaseUpdateInput)
|
||||
if err != nil {
|
||||
return input, errors.Wrap(err, "validate SharableVirtualResourceBaseUpdateInput")
|
||||
}
|
||||
|
||||
if input.LLMId != nil && strings.TrimSpace(*input.LLMId) != "" {
|
||||
llmId := strings.TrimSpace(*input.LLMId)
|
||||
llmUrl := ""
|
||||
model := ""
|
||||
if input.LLMUrl != nil {
|
||||
llmUrl = *input.LLMUrl
|
||||
}
|
||||
if input.Model != nil {
|
||||
model = *input.Model
|
||||
}
|
||||
if err := resolveRouterAgentLLM(ctx, userCred, query, &llmId, &llmUrl, &model); err != nil {
|
||||
return input, err
|
||||
}
|
||||
input.LLMId = &llmId
|
||||
input.LLMUrl = &llmUrl
|
||||
input.Model = &model
|
||||
}
|
||||
if input.LLMDriver != nil {
|
||||
driver := normalizeRouterLLMDriver(*input.LLMDriver)
|
||||
if !api.IsLLMClientType(driver) {
|
||||
return input, errors.Wrapf(httperrors.ErrInputParameter, "llm_driver must be one of: %s, got: %s", api.LLM_CLIENT_TYPES.List(), driver)
|
||||
}
|
||||
input.LLMDriver = &driver
|
||||
}
|
||||
if input.LLMUrl != nil {
|
||||
llmUrl := strings.TrimSpace(*input.LLMUrl)
|
||||
if llmUrl == "" {
|
||||
return input, errors.Wrap(httperrors.ErrInputParameter, "llm_url cannot be empty")
|
||||
}
|
||||
input.LLMUrl = &llmUrl
|
||||
}
|
||||
if input.Model != nil {
|
||||
model := strings.TrimSpace(*input.Model)
|
||||
if model == "" {
|
||||
return input, errors.Wrap(httperrors.ErrInputParameter, "model cannot be empty")
|
||||
}
|
||||
input.Model = &model
|
||||
}
|
||||
if input.DefaultRoute != nil {
|
||||
route := normalizeRouterRoute(*input.DefaultRoute)
|
||||
input.DefaultRoute = &route
|
||||
}
|
||||
if err := agent.normalizeRouterUpdateMaxPromptChars(ctx, &input); err != nil {
|
||||
return input, err
|
||||
}
|
||||
if input.MaxDecisionTokens != nil && *input.MaxDecisionTokens <= 0 {
|
||||
val := api.LLM_ROUTER_DEFAULT_MAX_DECISION_TOKENS
|
||||
input.MaxDecisionTokens = &val
|
||||
}
|
||||
if input.CandidateMapping != nil {
|
||||
mapping := normalizeCandidateMapping(*input.CandidateMapping)
|
||||
input.CandidateMapping = &mapping
|
||||
}
|
||||
normalizeRouterPromptUpdate(&input)
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func resolveRouterAgentLLM(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, llmId *string, llmUrl *string, model *string) error {
|
||||
if llmId == nil || strings.TrimSpace(*llmId) == "" {
|
||||
return nil
|
||||
}
|
||||
llmObj, err := GetLLMManager().FetchByIdOrName(ctx, userCred, strings.TrimSpace(*llmId))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "fetch LLM by id %s", *llmId)
|
||||
}
|
||||
llm := llmObj.(*SLLM)
|
||||
*llmId = llm.Id
|
||||
info, err := llm.GetLLMAccessUrlInfo(ctx, userCred, query)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "get LLM URL from LLM %s", *llmId)
|
||||
}
|
||||
*llmUrl = info.LoginUrl
|
||||
if strings.TrimSpace(*model) != "" {
|
||||
return nil
|
||||
}
|
||||
mdlInfos, err := llm.getProbedInstantModelsExt(ctx, userCred)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get probed models from LLM instance")
|
||||
}
|
||||
if len(mdlInfos) == 0 {
|
||||
return httperrors.NewBadRequestError("no available models found in LLM instance %s", *llmId)
|
||||
}
|
||||
var first api.LLMInternalInstantMdlInfo
|
||||
for _, mdlInfo := range mdlInfos {
|
||||
first = mdlInfo
|
||||
break
|
||||
}
|
||||
*model = fmt.Sprintf("%s:%s", first.Name, first.Tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeRouterLLMDriver(driver string) string {
|
||||
driver = strings.ToLower(strings.TrimSpace(driver))
|
||||
if driver == "" {
|
||||
return string(api.LLM_CLIENT_OPENAI)
|
||||
}
|
||||
return driver
|
||||
}
|
||||
|
||||
func normalizeRouterRoute(route string) string {
|
||||
route = strings.ToLower(strings.TrimSpace(route))
|
||||
if route == "" {
|
||||
return api.LLM_ROUTER_DEFAULT_ROUTE
|
||||
}
|
||||
return route
|
||||
}
|
||||
|
||||
func normalizePositiveInt(val int, def int) int {
|
||||
if val <= 0 {
|
||||
return def
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
const routerModelsTimeout = 3 * time.Second
|
||||
|
||||
type routerModelsResponse struct {
|
||||
Data []routerModelEntry `json:"data"`
|
||||
}
|
||||
|
||||
type routerModelEntry struct {
|
||||
ID string `json:"id"`
|
||||
MaxModelLen int `json:"max_model_len"`
|
||||
ContextLength int `json:"context_length"`
|
||||
}
|
||||
|
||||
func buildRouterModelsURL(endpoint string) (string, error) {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if endpoint == "" {
|
||||
return "", errors.Error("llm_url is empty")
|
||||
}
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "parse llm_url")
|
||||
}
|
||||
if u.Scheme == "" || u.Host == "" {
|
||||
return "", errors.Error("llm_url must be absolute")
|
||||
}
|
||||
u.RawQuery = ""
|
||||
u.Fragment = ""
|
||||
path := strings.TrimRight(u.Path, "/")
|
||||
switch {
|
||||
case path == "":
|
||||
u.Path = "/v1/models"
|
||||
case strings.HasSuffix(path, "/v1/models"):
|
||||
u.Path = path
|
||||
case strings.HasSuffix(path, "/v1"):
|
||||
u.Path = path + "/models"
|
||||
default:
|
||||
u.Path = path + "/v1/models"
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func fetchRouterContextWindow(ctx context.Context, client *http.Client, endpoint, model, apiKey string) (int, error) {
|
||||
modelsURL, err := buildRouterModelsURL(endpoint)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, modelsURL, nil)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "create /v1/models request")
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if apiKey = strings.TrimSpace(apiKey); apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "request /v1/models")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return 0, errors.Errorf("/v1/models returned status %d", resp.StatusCode)
|
||||
}
|
||||
var out routerModelsResponse
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&out); err != nil {
|
||||
return 0, errors.Wrap(err, "decode /v1/models response")
|
||||
}
|
||||
model = strings.TrimSpace(model)
|
||||
for _, item := range out.Data {
|
||||
if strings.TrimSpace(item.ID) != model {
|
||||
continue
|
||||
}
|
||||
if item.MaxModelLen > 0 {
|
||||
return item.MaxModelLen, nil
|
||||
}
|
||||
if item.ContextLength > 0 {
|
||||
return item.ContextLength, nil
|
||||
}
|
||||
return 0, errors.Errorf("model %q has no valid context length", model)
|
||||
}
|
||||
return 0, errors.Errorf("model %q not found in /v1/models", model)
|
||||
}
|
||||
|
||||
func routerMaxPromptChars(contextTokens int) int {
|
||||
if contextTokens <= 0 {
|
||||
contextTokens = api.LLM_DEFAULT_CONTEXT_TOKENS
|
||||
}
|
||||
return contextTokens * 3 / 4
|
||||
}
|
||||
|
||||
func resolveRouterMaxPromptChars(ctx context.Context, requested int, endpoint, model, apiKey string) (int, error) {
|
||||
if requested < 0 {
|
||||
return 0, errors.Wrapf(httperrors.ErrInputParameter, "max_prompt_chars must be >= 0, got %d", requested)
|
||||
}
|
||||
if requested > 0 {
|
||||
return requested, nil
|
||||
}
|
||||
contextTokens, err := fetchRouterContextWindow(ctx, &http.Client{Timeout: routerModelsTimeout}, endpoint, model, apiKey)
|
||||
if err != nil {
|
||||
log.Warningf("resolve llm router context window for model %q failed: %v; using default %d", model, err, api.LLM_DEFAULT_CONTEXT_TOKENS)
|
||||
contextTokens = api.LLM_DEFAULT_CONTEXT_TOKENS
|
||||
}
|
||||
return routerMaxPromptChars(contextTokens), nil
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) normalizeRouterUpdateMaxPromptChars(ctx context.Context, input *api.LLMRouterAgentUpdateInput) error {
|
||||
if input.MaxPromptChars == nil {
|
||||
return nil
|
||||
}
|
||||
requested := *input.MaxPromptChars
|
||||
if requested != 0 {
|
||||
maxPromptChars, err := resolveRouterMaxPromptChars(ctx, requested, "", "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
input.MaxPromptChars = &maxPromptChars
|
||||
return nil
|
||||
}
|
||||
|
||||
llmURL, model := agent.LLMUrl, agent.Model
|
||||
if input.LLMUrl != nil {
|
||||
llmURL = *input.LLMUrl
|
||||
}
|
||||
if input.Model != nil {
|
||||
model = *input.Model
|
||||
}
|
||||
apiKey := ""
|
||||
if input.ApiKey != nil {
|
||||
apiKey = *input.ApiKey
|
||||
} else {
|
||||
storedAPIKey, err := agent.GetApiKey()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "decrypt llm router api key")
|
||||
}
|
||||
apiKey = storedAPIKey
|
||||
}
|
||||
maxPromptChars, err := resolveRouterMaxPromptChars(ctx, 0, llmURL, model, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
input.MaxPromptChars = &maxPromptChars
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeCandidateMapping(in map[string][]string) map[string][]string {
|
||||
out := make(map[string][]string, len(in))
|
||||
for route, models := range in {
|
||||
route = normalizeRouterRoute(route)
|
||||
for _, model := range models {
|
||||
model = strings.TrimSpace(model)
|
||||
if model != "" {
|
||||
out[route] = append(out[route], model)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeRouterPromptConfig(cfg api.LLMRouterPromptConfig) api.LLMRouterPromptConfig {
|
||||
cfg.SimpleDefinition = normalizeRouterDefinition(cfg.SimpleDefinition, api.LLM_ROUTER_DEFAULT_SIMPLE_DEFINITION)
|
||||
cfg.ComplexDefinition = normalizeRouterDefinition(cfg.ComplexDefinition, api.LLM_ROUTER_DEFAULT_COMPLEX_DEFINITION)
|
||||
cfg.SimpleExamples = normalizeRouterExamples(cfg.SimpleExamples)
|
||||
cfg.ComplexExamples = normalizeRouterExamples(cfg.ComplexExamples)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func normalizeRouterDefinition(definition, fallback string) string {
|
||||
definition = strings.TrimSpace(definition)
|
||||
if definition == "" {
|
||||
return fallback
|
||||
}
|
||||
return definition
|
||||
}
|
||||
|
||||
func normalizeRouterExamples(in []string) []string {
|
||||
out := make([]string, 0, len(in))
|
||||
seen := map[string]struct{}{}
|
||||
for _, example := range in {
|
||||
example = strings.TrimSpace(example)
|
||||
if example == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[example]; ok {
|
||||
continue
|
||||
}
|
||||
seen[example] = struct{}{}
|
||||
out = append(out, example)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeRouterPromptUpdate(input *api.LLMRouterAgentUpdateInput) {
|
||||
if input.SimpleDefinition != nil {
|
||||
definition := normalizeRouterDefinition(*input.SimpleDefinition, api.LLM_ROUTER_DEFAULT_SIMPLE_DEFINITION)
|
||||
input.SimpleDefinition = &definition
|
||||
}
|
||||
if input.ComplexDefinition != nil {
|
||||
definition := normalizeRouterDefinition(*input.ComplexDefinition, api.LLM_ROUTER_DEFAULT_COMPLEX_DEFINITION)
|
||||
input.ComplexDefinition = &definition
|
||||
}
|
||||
if input.SimpleExamples != nil {
|
||||
examples := normalizeRouterExamples(*input.SimpleExamples)
|
||||
input.SimpleExamples = &examples
|
||||
}
|
||||
if input.ComplexExamples != nil {
|
||||
examples := normalizeRouterExamples(*input.ComplexExamples)
|
||||
input.ComplexExamples = &examples
|
||||
}
|
||||
}
|
||||
|
||||
func (man *SLLMRouterAgentManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.LLMRouterAgentListInput) (*sqlchemy.SQuery, error) {
|
||||
q, err := man.SSharableVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, input.SharableVirtualResourceListInput)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "SSharableVirtualResourceBaseManager.ListItemFilter")
|
||||
}
|
||||
if len(input.LLMDriver) > 0 {
|
||||
q = q.Equals("llm_driver", normalizeRouterLLMDriver(input.LLMDriver))
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (manager *SLLMRouterAgentManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.LLMRouterAgentDetails {
|
||||
rows := make([]api.LLMRouterAgentDetails, len(objs))
|
||||
vrows := manager.SSharableVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
|
||||
agents := []SLLMRouterAgent{}
|
||||
jsonutils.Update(&agents, objs)
|
||||
|
||||
llmIds := make([]string, 0)
|
||||
for i := range agents {
|
||||
if agents[i].LLMId != "" {
|
||||
llmIds = append(llmIds, agents[i].LLMId)
|
||||
}
|
||||
}
|
||||
var llmIdNameMap map[string]string
|
||||
if len(llmIds) > 0 {
|
||||
var err error
|
||||
llmIdNameMap, err = db.FetchIdNameMap2(GetLLMManager(), llmIds)
|
||||
if err != nil {
|
||||
log.Errorf("FetchIdNameMap2 for LLMs failed: %v", err)
|
||||
}
|
||||
}
|
||||
for i := range rows {
|
||||
rows[i].SharableVirtualResourceDetails = vrows[i]
|
||||
if i < len(agents) {
|
||||
rows[i].LLMId = agents[i].LLMId
|
||||
if name, ok := llmIdNameMap[agents[i].LLMId]; ok {
|
||||
rows[i].LLMName = name
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) PerformRoute(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.LLMRouterRouteRequest) (jsonutils.JSONObject, error) {
|
||||
out, err := agent.Route(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonutils.Marshal(out), nil
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) PerformExpandExamples(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.LLMRouterExpandExamplesInput) (jsonutils.JSONObject, error) {
|
||||
cfg := agent.promptConfig()
|
||||
applyRouterPromptDraft(&cfg, input)
|
||||
cfg = normalizeRouterPromptConfig(cfg)
|
||||
content, err := callRouterAgentModel(ctx, agent, routerExpandSystemPrompt, buildRouterExpandPrompt(cfg))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "expand router examples")
|
||||
}
|
||||
out, err := parseRouterPromptConfig(content)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parse expanded router examples")
|
||||
}
|
||||
return jsonutils.Marshal(out), nil
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) Route(ctx context.Context, req api.LLMRouterRouteRequest) (*api.LLMRouterRouteResponse, error) {
|
||||
if len(req.Candidates) == 0 {
|
||||
return nil, errors.Wrap(httperrors.ErrInputParameter, "candidates is required")
|
||||
}
|
||||
route, ok := agent.routeByRules(req)
|
||||
if !ok {
|
||||
route = agent.routeByDecisionModel(ctx, req)
|
||||
}
|
||||
if route == "" {
|
||||
route = normalizeRouterRoute(agent.DefaultRoute)
|
||||
}
|
||||
model := agent.pickCandidate(route, req.Candidates)
|
||||
if model == "" {
|
||||
return nil, errors.Wrap(httperrors.ErrInvalidStatus, "no selectable candidate model")
|
||||
}
|
||||
return &api.LLMRouterRouteResponse{Model: model}, nil
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) routeByRules(req api.LLMRouterRouteRequest) (string, bool) {
|
||||
prompt := agent.requestText(req)
|
||||
maxChars := normalizePositiveInt(agent.MaxPromptChars, api.LLM_ROUTER_DEFAULT_MAX_PROMPT_CHARS)
|
||||
if len([]rune(prompt)) > maxChars {
|
||||
return api.LLM_ROUTER_ROUTE_COMPLEX, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
const (
|
||||
routerDecisionSystemPrompt = "你是模型分流器。根据给定定义和样例把请求分类为 simple 或 complex。只能输出一个 JSON 对象,不得输出 Markdown、代码块或额外解释。"
|
||||
routerExpandSystemPrompt = "你是模型分流规则编辑器。规范 simple 和 complex 的定义并补充代表性样例。只能输出指定的 JSON 对象,不得输出 Markdown、代码块或额外解释。"
|
||||
)
|
||||
|
||||
var callRouterAgentModel = func(ctx context.Context, agent *SLLMRouterAgent, systemPrompt, userPrompt string) (string, error) {
|
||||
if strings.TrimSpace(agent.LLMUrl) == "" || strings.TrimSpace(agent.Model) == "" {
|
||||
return "", errors.Wrap(httperrors.ErrInvalidStatus, "router model URL and model are required")
|
||||
}
|
||||
driver, err := GetLLMClientDriverWithError(api.LLMClientType(normalizeRouterLLMDriver(agent.LLMDriver)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
apiKey, err := agent.GetApiKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmp := &SMCPAgent{
|
||||
LLMUrl: agent.LLMUrl,
|
||||
LLMDriver: normalizeRouterLLMDriver(agent.LLMDriver),
|
||||
Model: agent.Model,
|
||||
ApiKey: apiKey,
|
||||
}
|
||||
timeout := time.Duration(options.Options.MCPAgentTimeout) * time.Second
|
||||
if timeout <= 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
callCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
resp, err := driver.Chat(callCtx, tmp, []ILLMChatMessage{
|
||||
driver.NewSystemMessage(systemPrompt),
|
||||
driver.NewUserMessage(userPrompt),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GetContent(), nil
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) routeByDecisionModel(ctx context.Context, req api.LLMRouterRouteRequest) string {
|
||||
prompt := agent.BuildDecisionPrompt(req)
|
||||
content, err := callRouterAgentModel(ctx, agent, routerDecisionSystemPrompt, prompt)
|
||||
if err != nil {
|
||||
log.Warningf("llm router decision model failed: %v", err)
|
||||
return normalizeRouterRoute(agent.DefaultRoute)
|
||||
}
|
||||
decision, err := parseRouterDecision(content)
|
||||
if err != nil || decision.Confidence < 0.5 {
|
||||
if err != nil {
|
||||
log.Warningf("parse llm router decision failed: %v", err)
|
||||
}
|
||||
return normalizeRouterRoute(agent.DefaultRoute)
|
||||
}
|
||||
return normalizeRouterRoute(decision.Route)
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) BuildDecisionPrompt(req api.LLMRouterRouteRequest) string {
|
||||
promptConfig := agent.promptConfig()
|
||||
maxTokens := normalizePositiveInt(agent.MaxDecisionTokens, api.LLM_ROUTER_DEFAULT_MAX_DECISION_TOKENS)
|
||||
return fmt.Sprintf(`根据以下定义和样例选择 route。
|
||||
|
||||
simple 定义:
|
||||
%s
|
||||
|
||||
complex 定义:
|
||||
%s
|
||||
|
||||
simple 样例:
|
||||
%s
|
||||
|
||||
complex 样例:
|
||||
%s
|
||||
|
||||
候选模型:%s
|
||||
最多输出约 %d tokens。
|
||||
|
||||
只返回 JSON,不要解释:
|
||||
{"route":"simple or complex","confidence":0.0,"reason":"short reason"}
|
||||
|
||||
请求内容:
|
||||
%s`, promptConfig.SimpleDefinition, promptConfig.ComplexDefinition, formatRouterExamples(promptConfig.SimpleExamples), formatRouterExamples(promptConfig.ComplexExamples), strings.Join(req.Candidates, ", "), maxTokens, agent.requestText(req))
|
||||
}
|
||||
|
||||
func buildRouterExpandPrompt(cfg api.LLMRouterPromptConfig) string {
|
||||
return fmt.Sprintf(`请规范以下二分类定义和样例,并补充少量有代表性的样例。保持 simple 和 complex 边界互斥且清晰,不要增加其他 route。
|
||||
|
||||
simple 定义:
|
||||
%s
|
||||
|
||||
complex 定义:
|
||||
%s
|
||||
|
||||
simple 样例:
|
||||
%s
|
||||
|
||||
complex 样例:
|
||||
%s
|
||||
|
||||
只返回:
|
||||
{"simple_definition":"...","complex_definition":"...","simple_examples":["..."],"complex_examples":["..."]}`,
|
||||
cfg.SimpleDefinition,
|
||||
cfg.ComplexDefinition,
|
||||
formatRouterExamples(cfg.SimpleExamples),
|
||||
formatRouterExamples(cfg.ComplexExamples),
|
||||
)
|
||||
}
|
||||
|
||||
func applyRouterPromptDraft(cfg *api.LLMRouterPromptConfig, input api.LLMRouterExpandExamplesInput) {
|
||||
if input.SimpleDefinition != nil {
|
||||
cfg.SimpleDefinition = *input.SimpleDefinition
|
||||
}
|
||||
if input.ComplexDefinition != nil {
|
||||
cfg.ComplexDefinition = *input.ComplexDefinition
|
||||
}
|
||||
if input.SimpleExamples != nil {
|
||||
cfg.SimpleExamples = *input.SimpleExamples
|
||||
}
|
||||
if input.ComplexExamples != nil {
|
||||
cfg.ComplexExamples = *input.ComplexExamples
|
||||
}
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) promptConfig() api.LLMRouterPromptConfig {
|
||||
cfg := api.LLMRouterPromptConfig{
|
||||
SimpleDefinition: agent.SimpleDefinition,
|
||||
ComplexDefinition: agent.ComplexDefinition,
|
||||
}
|
||||
if agent.SimpleExamples != nil {
|
||||
if err := agent.SimpleExamples.Unmarshal(&cfg.SimpleExamples); err != nil {
|
||||
log.Warningf("invalid simple_examples for llm router agent %s: %v", agent.Id, err)
|
||||
}
|
||||
}
|
||||
if agent.ComplexExamples != nil {
|
||||
if err := agent.ComplexExamples.Unmarshal(&cfg.ComplexExamples); err != nil {
|
||||
log.Warningf("invalid complex_examples for llm router agent %s: %v", agent.Id, err)
|
||||
}
|
||||
}
|
||||
return normalizeRouterPromptConfig(cfg)
|
||||
}
|
||||
|
||||
func formatRouterExamples(examples []string) string {
|
||||
if len(examples) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return "- " + strings.Join(examples, "\n- ")
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) requestText(req api.LLMRouterRouteRequest) string {
|
||||
parts := make([]string, 0, len(req.Messages))
|
||||
for _, msg := range req.Messages {
|
||||
switch val := msg.Content.(type) {
|
||||
case string:
|
||||
parts = append(parts, val)
|
||||
default:
|
||||
parts = append(parts, jsonutils.Marshal(val).String())
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) candidateMapping() map[string][]string {
|
||||
ret := map[string][]string{}
|
||||
if agent.CandidateMapping == nil {
|
||||
return ret
|
||||
}
|
||||
if err := agent.CandidateMapping.Unmarshal(&ret); err != nil {
|
||||
log.Warningf("invalid llm router candidate_mapping: %v", err)
|
||||
return map[string][]string{}
|
||||
}
|
||||
return normalizeCandidateMapping(ret)
|
||||
}
|
||||
|
||||
func (agent *SLLMRouterAgent) pickCandidate(route string, candidates []string) string {
|
||||
mapping := agent.candidateMapping()
|
||||
if model := pickCandidateFromList(mapping[normalizeRouterRoute(route)], candidates); model != "" {
|
||||
return model
|
||||
}
|
||||
def := normalizeRouterRoute(agent.DefaultRoute)
|
||||
if def != normalizeRouterRoute(route) {
|
||||
if model := pickCandidateFromList(mapping[def], candidates); model != "" {
|
||||
return model
|
||||
}
|
||||
}
|
||||
if model := pickCandidateFromList([]string{route}, candidates); model != "" {
|
||||
return model
|
||||
}
|
||||
return strings.TrimSpace(candidates[0])
|
||||
}
|
||||
|
||||
func pickCandidateFromList(models []string, candidates []string) string {
|
||||
for _, model := range models {
|
||||
model = strings.TrimSpace(model)
|
||||
for _, candidate := range candidates {
|
||||
if strings.EqualFold(strings.TrimSpace(candidate), model) {
|
||||
return strings.TrimSpace(candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseRouterDecision(content string) (*api.LLMRouterDecision, error) {
|
||||
content = extractRouterJSON(content)
|
||||
decision := api.LLMRouterDecision{}
|
||||
if err := json.Unmarshal([]byte(content), &decision); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decision.Route = normalizeRouterRoute(decision.Route)
|
||||
if decision.Route == "" {
|
||||
return nil, errors.Error("missing route")
|
||||
}
|
||||
if decision.Confidence == 0 {
|
||||
decision.Confidence = 1
|
||||
}
|
||||
return &decision, nil
|
||||
}
|
||||
|
||||
func parseRouterPromptConfig(content string) (*api.LLMRouterPromptConfig, error) {
|
||||
var raw struct {
|
||||
SimpleDefinition *string `json:"simple_definition"`
|
||||
ComplexDefinition *string `json:"complex_definition"`
|
||||
SimpleExamples *[]string `json:"simple_examples"`
|
||||
ComplexExamples *[]string `json:"complex_examples"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(extractRouterJSON(content)), &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if raw.SimpleDefinition == nil || raw.ComplexDefinition == nil || raw.SimpleExamples == nil || raw.ComplexExamples == nil {
|
||||
return nil, errors.Error("router expansion response missing required fields")
|
||||
}
|
||||
cfg := normalizeRouterPromptConfig(api.LLMRouterPromptConfig{
|
||||
SimpleDefinition: *raw.SimpleDefinition,
|
||||
ComplexDefinition: *raw.ComplexDefinition,
|
||||
SimpleExamples: *raw.SimpleExamples,
|
||||
ComplexExamples: *raw.ComplexExamples,
|
||||
})
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func extractRouterJSON(content string) string {
|
||||
content = strings.TrimSpace(content)
|
||||
content = strings.TrimPrefix(content, "```json")
|
||||
content = strings.TrimPrefix(content, "```")
|
||||
content = strings.TrimSuffix(content, "```")
|
||||
content = strings.TrimSpace(content)
|
||||
if start := strings.Index(content, "{"); start >= 0 {
|
||||
if end := strings.LastIndex(content, "}"); end > start {
|
||||
content = content[start : end+1]
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
@@ -304,6 +304,41 @@ func handleDefaultMcpTools(ctx context.Context, w http.ResponseWriter, r *http.R
|
||||
appsrv.SendJSON(w, result)
|
||||
}
|
||||
|
||||
func handleLLMRouterAgentRoute(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
params, _, body := appsrv.FetchEnv(ctx, w, r)
|
||||
id := params["<id>"]
|
||||
if id == "" {
|
||||
httperrors.MissingParameterError(ctx, w, "id")
|
||||
return
|
||||
}
|
||||
if body == nil {
|
||||
body = jsonutils.NewDict()
|
||||
}
|
||||
if body.Contains(models.GetLLMRouterAgentManager().Keyword()) {
|
||||
agentObj, _ := body.Get(models.GetLLMRouterAgentManager().Keyword())
|
||||
if agentObj != nil {
|
||||
body = agentObj
|
||||
}
|
||||
}
|
||||
input := api.LLMRouterRouteRequest{}
|
||||
if err := body.Unmarshal(&input); err != nil {
|
||||
httperrors.InvalidInputError(ctx, w, "invalid input: %v", err)
|
||||
return
|
||||
}
|
||||
obj, err := models.GetLLMRouterAgentManager().FetchByIdOrName(ctx, nil, id)
|
||||
if err != nil {
|
||||
httperrors.GeneralServerError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
agent := obj.(*models.SLLMRouterAgent)
|
||||
out, err := agent.Route(ctx, input)
|
||||
if err != nil {
|
||||
httperrors.GeneralServerError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
appsrv.SendStruct(w, out)
|
||||
}
|
||||
|
||||
func InitHandlers(app *appsrv.Application, isSlave bool) {
|
||||
db.InitAllManagers()
|
||||
db.RegistUserCredCacheUpdater()
|
||||
@@ -340,6 +375,8 @@ func InitHandlers(app *appsrv.Application, isSlave bool) {
|
||||
// 默认 MCP 服务器 tools:仅使用 options.MCPServerURL,不依赖 mcp_agent 条目
|
||||
app.AddHandler2("GET", "/mcp_agents/default-mcp-tools", auth.Authenticate(handleDefaultMcpTools), nil, "default_mcp_tools", nil)
|
||||
|
||||
app.AddHandler2("POST", "/llm_router_agents/<id>/route", handleLLMRouterAgentRoute, nil, "llm_router_agent_route", nil)
|
||||
|
||||
for _, manager := range []db.IModelManager{
|
||||
taskman.TaskManager,
|
||||
taskman.SubTaskManager,
|
||||
@@ -372,6 +409,7 @@ func InitHandlers(app *appsrv.Application, isSlave bool) {
|
||||
models.GetInstantModelManager(),
|
||||
models.GetLLMInstantModelManager(),
|
||||
models.GetMCPAgentManager(),
|
||||
models.GetLLMRouterAgentManager(),
|
||||
} {
|
||||
db.RegisterModelManager(manager)
|
||||
handler := db.NewModelHandler(manager)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
||||
)
|
||||
|
||||
type LLMRouterAgentManager struct {
|
||||
modulebase.ResourceManager
|
||||
}
|
||||
|
||||
var (
|
||||
LLMRouterAgent LLMRouterAgentManager
|
||||
)
|
||||
|
||||
func init() {
|
||||
LLMRouterAgent = LLMRouterAgentManager{
|
||||
ResourceManager: modules.NewLLMManager("llm_router_agent", "llm_router_agents",
|
||||
[]string{},
|
||||
[]string{},
|
||||
),
|
||||
}
|
||||
modules.Register(&LLMRouterAgent)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func (o *LLMImageShowOptions) Params() (jsonutils.JSONObject, error) {
|
||||
type LLMImageListOptions struct {
|
||||
options.BaseListOptions
|
||||
|
||||
LLMType string `json:"llm_type" choices:"ollama|dify|comfyui|vllm|sglang|hermes-agent|llm-router|desktop|benchmark" help:"filter by llm type"`
|
||||
LLMType string `json:"llm_type" choices:"ollama|dify|comfyui|vllm|sglang|hermes-agent|desktop|benchmark" help:"filter by llm type"`
|
||||
}
|
||||
|
||||
func (o *LLMImageListOptions) Params() (jsonutils.JSONObject, error) {
|
||||
@@ -30,7 +30,7 @@ type LLMImageCreateOptions struct {
|
||||
IMAGE_NAME string `json:"image_name"`
|
||||
IMAGE_LABEL string `json:"image_label"`
|
||||
CredentialId string `json:"credential_id"`
|
||||
LLM_TYPE string `json:"llm_type" choices:"ollama|dify|comfyui|vllm|sglang|hermes-agent|llm-router|desktop|benchmark" help:"llm type: ollama, comfyui, vllm, sglang, hermes-agent, llm-router, desktop, benchmark or dify"`
|
||||
LLM_TYPE string `json:"llm_type" choices:"ollama|dify|comfyui|vllm|sglang|hermes-agent|desktop|benchmark" help:"llm type: ollama, comfyui, vllm, sglang, hermes-agent, desktop, benchmark or dify"`
|
||||
}
|
||||
|
||||
func (o *LLMImageCreateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
@@ -44,7 +44,7 @@ type LLMImageUpdateOptions struct {
|
||||
ImageName string `json:"image_name"`
|
||||
ImageLabel string `json:"image_label"`
|
||||
CredentialId string `json:"credential_id"`
|
||||
LlmType string `json:"llm_type" choices:"ollama|dify|vllm|sglang|comfyui|hermes-agent|llm-router|desktop|benchmark" help:"llm type: ollama, comfyui, vllm, sglang, hermes-agent, llm-router, desktop, benchmark or dify"`
|
||||
LlmType string `json:"llm_type" choices:"ollama|dify|vllm|sglang|comfyui|hermes-agent|desktop|benchmark" help:"llm type: ollama, comfyui, vllm, sglang, hermes-agent, desktop, benchmark or dify"`
|
||||
AppName string `json:"app_name" help:"desktop application identifier (LinuxServer image id), e.g. firefox, chromium, steam, webtop-ubuntu-xfce"`
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ type LLMDeploymentCreateOptions struct {
|
||||
// Mode B/C: SKU spec (all sku-* flags collapse into sku_spec)
|
||||
SkuName string `help:"SKU name (default: <deploy-name>-sku)" json:"-"`
|
||||
SkuLLMImageId string `help:"container image id (for SkuSpec)" json:"-"`
|
||||
SkuLLMType string `help:"container llm type" choices:"ollama|vllm|comfyui|sglang|dify|llm-router" json:"-"`
|
||||
SkuLLMType string `help:"container llm type" choices:"ollama|vllm|comfyui|sglang|dify" json:"-"`
|
||||
SkuCpu int `help:"SKU CPU cores" json:"-"`
|
||||
SkuMemory int `help:"SKU memory MB" json:"-"`
|
||||
SkuDiskSize int `help:"SKU disk size MB" json:"-"`
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/apis"
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/options"
|
||||
)
|
||||
|
||||
type LLMRouterAgentListOptions struct {
|
||||
options.BaseListOptions
|
||||
|
||||
LLMDriver string `json:"llm_driver" help:"filter by llm driver"`
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentListOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return options.ListStructToParams(o)
|
||||
}
|
||||
|
||||
type LLMRouterAgentShowOptions struct {
|
||||
options.BaseShowOptions
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentShowOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return options.StructToParams(o)
|
||||
}
|
||||
|
||||
type LLMRouterAgentCreateOptions struct {
|
||||
apis.SharableVirtualResourceCreateInput
|
||||
|
||||
LlmId string `help:"LLM instance ID; if set, llm_url is resolved from it" json:"llm_id"`
|
||||
LLM_URL string `help:"decision model OpenAI-compatible base URL" json:"llm_url"`
|
||||
LLM_DRIVER string `help:"decision model driver" json:"llm_driver" choices:"ollama|openai"`
|
||||
MODEL string `help:"decision model name" json:"model"`
|
||||
API_KEY string `help:"decision model API key" json:"api_key"`
|
||||
DefaultRoute string `help:"fallback route, default complex" json:"default_route"`
|
||||
MaxPromptChars int `help:"request char limit; 0 auto-detects /v1/models context, longer requests use complex" json:"max_prompt_chars"`
|
||||
MaxDecisionTokens int `help:"decision answer token hint" json:"max_decision_tokens"`
|
||||
CandidateMapping string `help:"candidate mapping JSON, e.g. '{\"simple\":[\"qwen\"],\"complex\":[\"deepseek\"]}'" json:"-"`
|
||||
SimpleDefinition string `help:"definition of simple requests" json:"simple_definition"`
|
||||
ComplexDefinition string `help:"definition of complex requests" json:"complex_definition"`
|
||||
SimpleExamples string `help:"simple examples JSON array" json:"-"`
|
||||
ComplexExamples string `help:"complex examples JSON array" json:"-"`
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentCreateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
obj := jsonutils.Marshal(o).(*jsonutils.JSONDict)
|
||||
if o.CandidateMapping != "" {
|
||||
mapping, err := parseRouterCandidateMapping(o.CandidateMapping)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj.Set("candidate_mapping", jsonutils.Marshal(mapping))
|
||||
}
|
||||
if err := setRouterExamples(obj, "simple_examples", o.SimpleExamples); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setRouterExamples(obj, "complex_examples", o.ComplexExamples); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
type LLMRouterAgentUpdateOptions struct {
|
||||
apis.SharableVirtualResourceBaseUpdateInput
|
||||
|
||||
ID string
|
||||
LlmId *string `help:"LLM instance ID; if set, llm_url is resolved from it" json:"llm_id,omitempty"`
|
||||
LlmUrl *string `help:"decision model OpenAI-compatible base URL" json:"llm_url,omitempty"`
|
||||
LlmDriver *string `help:"decision model driver" json:"llm_driver,omitempty" choices:"ollama|openai"`
|
||||
Model *string `help:"decision model name" json:"model,omitempty"`
|
||||
ApiKey *string `help:"decision model API key" json:"api_key,omitempty"`
|
||||
DefaultRoute *string `help:"fallback route" json:"default_route,omitempty"`
|
||||
MaxPromptChars *int `help:"request char limit; 0 recomputes from /v1/models context" json:"max_prompt_chars,omitempty"`
|
||||
MaxDecisionTokens *int `help:"decision answer token hint" json:"max_decision_tokens,omitempty"`
|
||||
CandidateMapping string `help:"candidate mapping JSON" json:"-"`
|
||||
SimpleDefinition *string `help:"definition of simple requests" json:"simple_definition,omitempty"`
|
||||
ComplexDefinition *string `help:"definition of complex requests" json:"complex_definition,omitempty"`
|
||||
SimpleExamples string `help:"replace simple examples with JSON array" json:"-"`
|
||||
ComplexExamples string `help:"replace complex examples with JSON array" json:"-"`
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentUpdateOptions) GetId() string {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentUpdateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
obj := jsonutils.Marshal(o).(*jsonutils.JSONDict)
|
||||
obj.Remove("id")
|
||||
if o.CandidateMapping != "" {
|
||||
mapping, err := parseRouterCandidateMapping(o.CandidateMapping)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj.Set("candidate_mapping", jsonutils.Marshal(mapping))
|
||||
}
|
||||
if err := setRouterExamples(obj, "simple_examples", o.SimpleExamples); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setRouterExamples(obj, "complex_examples", o.ComplexExamples); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
type LLMRouterAgentDeleteOptions struct {
|
||||
options.BaseIdOptions
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentDeleteOptions) GetId() string {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentDeleteOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return options.StructToParams(o)
|
||||
}
|
||||
|
||||
type LLMRouterAgentRouteOptions struct {
|
||||
ID string `help:"llm router agent id or name" json:"-"`
|
||||
Model string `help:"requested model" json:"model"`
|
||||
Message []string `help:"user message; repeatable" json:"-"`
|
||||
Candidates []string `help:"candidate model; repeatable" json:"candidates"`
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentRouteOptions) GetId() string {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentRouteOptions) Params() (jsonutils.JSONObject, error) {
|
||||
messages := make([]api.LLMRouterMessage, 0, len(o.Message))
|
||||
for _, msg := range o.Message {
|
||||
messages = append(messages, api.LLMRouterMessage{Role: "user", Content: msg})
|
||||
}
|
||||
input := api.LLMRouterRouteRequest{
|
||||
Model: o.Model,
|
||||
Messages: messages,
|
||||
Candidates: o.Candidates,
|
||||
}
|
||||
return jsonutils.Marshal(input), nil
|
||||
}
|
||||
|
||||
type LLMRouterAgentExpandExamplesOptions struct {
|
||||
ID string `help:"llm router agent id or name" json:"-"`
|
||||
SimpleDefinition *string `help:"draft definition of simple requests" json:"simple_definition,omitempty"`
|
||||
ComplexDefinition *string `help:"draft definition of complex requests" json:"complex_definition,omitempty"`
|
||||
SimpleExamples string `help:"draft simple examples JSON array" json:"-"`
|
||||
ComplexExamples string `help:"draft complex examples JSON array" json:"-"`
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentExpandExamplesOptions) GetId() string {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
func (o *LLMRouterAgentExpandExamplesOptions) Params() (jsonutils.JSONObject, error) {
|
||||
obj := jsonutils.Marshal(o).(*jsonutils.JSONDict)
|
||||
obj.Remove("id")
|
||||
if err := setRouterExamples(obj, "simple_examples", o.SimpleExamples); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setRouterExamples(obj, "complex_examples", o.ComplexExamples); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func parseRouterCandidateMapping(raw string) (map[string][]string, error) {
|
||||
obj, err := jsonutils.ParseString(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse candidate mapping JSON: %v", err)
|
||||
}
|
||||
mapping := map[string][]string{}
|
||||
if err := obj.Unmarshal(&mapping); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal candidate mapping: %v", err)
|
||||
}
|
||||
return mapping, nil
|
||||
}
|
||||
|
||||
func setRouterExamples(obj *jsonutils.JSONDict, key, raw string) error {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
examples, err := parseRouterExamples(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj.Set(key, jsonutils.Marshal(examples))
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRouterExamples(raw string) ([]string, error) {
|
||||
obj, err := jsonutils.ParseString(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse router examples JSON: %v", err)
|
||||
}
|
||||
examples := []string{}
|
||||
if err := obj.Unmarshal(&examples); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal router examples: %v", err)
|
||||
}
|
||||
return examples, nil
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type LLMSkuListOptions struct {
|
||||
options.BaseListOptions
|
||||
|
||||
LLMType string `json:"llm_type" choices:"ollama|vllm|sglang|dify|comfyui|openclaw|hermes-agent|llm-router|desktop"`
|
||||
LLMType string `json:"llm_type" choices:"ollama|vllm|sglang|dify|comfyui|openclaw|hermes-agent|desktop"`
|
||||
Source string `json:"source" help:"filter by source (huggingface, model_scope, local_path)"`
|
||||
Categories string `json:"categories" help:"filter by category (llm, embedding, image, ...)"`
|
||||
}
|
||||
@@ -36,7 +36,7 @@ type LLMSkuCreateOptions struct {
|
||||
MountedModels []string `help:"mounted models, <model_id> e.g. qwen2:0.5b-dup" json:"mounted_models"`
|
||||
|
||||
LLM_IMAGE_ID string `json:"llm_image_id"`
|
||||
LLM_TYPE string `json:"llm_type" choices:"ollama|vllm|sglang|comfyui|hermes-agent|llm-router|desktop"`
|
||||
LLM_TYPE string `json:"llm_type" choices:"ollama|vllm|sglang|comfyui|hermes-agent|desktop"`
|
||||
|
||||
// Model source
|
||||
Source string `help:"model source: huggingface, model_scope, local_path" json:"source"`
|
||||
@@ -67,14 +67,6 @@ type LLMSkuCreateOptions struct {
|
||||
HermesModel string `token:"hermes-model" help:"model name for Hermes" json:"-"`
|
||||
HermesApiKey string `token:"hermes-api-key" help:"API key for Hermes custom provider; defaults to EMPTY when omitted" json:"-"`
|
||||
HermesContextLength int `token:"hermes-context-length" help:"Hermes model.context_length" json:"-"`
|
||||
|
||||
RouterMethod string `token:"router-method" help:"LLM router method, e.g. routerdc" json:"-"`
|
||||
RouterConfigPath string `token:"router-config-path" help:"LLM router config path inside container" json:"-"`
|
||||
RouterModelDir string `token:"router-model-dir" help:"LLM router model directory inside container" json:"-"`
|
||||
RouterRoutePath string `token:"router-route-path" help:"LLM router route API path, e.g. /v1/route" json:"-"`
|
||||
RouterHealthPath string `token:"router-health-path" help:"LLM router health API path, e.g. /health" json:"-"`
|
||||
RouterCandidateMappingPath string `token:"router-candidate-mapping-path" help:"LLM router candidate mapping path inside container" json:"-"`
|
||||
RouterEnv []string `token:"router-env" help:"LLM router env in format key=value; repeatable" json:"-"`
|
||||
}
|
||||
|
||||
func (o *LLMSkuCreateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
@@ -134,49 +126,10 @@ func (o *LLMSkuCreateOptions) Params() (jsonutils.JSONObject, error) {
|
||||
spec := &api.LLMSpec{HermesAgent: hermesSpec}
|
||||
dict.Set("llm_spec", jsonutils.Marshal(spec))
|
||||
}
|
||||
case string(api.LLM_CONTAINER_LLM_ROUTER):
|
||||
routerSpec, err := newLLMRouterSpecFromArgs(o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dict.Set("llm_spec", jsonutils.Marshal(&api.LLMSpec{LLMRouter: routerSpec}))
|
||||
}
|
||||
return dict, nil
|
||||
}
|
||||
|
||||
func newLLMRouterSpecFromArgs(o *LLMSkuCreateOptions) (*api.LLMSpecLLMRouter, error) {
|
||||
method := strings.TrimSpace(o.RouterMethod)
|
||||
if method == "" {
|
||||
return nil, errors.Error("--router-method is required when llm_type=llm-router")
|
||||
}
|
||||
envs, err := newLLMRouterEnvsFromArgs(o.RouterEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &api.LLMSpecLLMRouter{
|
||||
RouterMethod: method,
|
||||
ConfigPath: strings.TrimSpace(o.RouterConfigPath),
|
||||
ModelDir: strings.TrimSpace(o.RouterModelDir),
|
||||
RoutePath: strings.TrimSpace(o.RouterRoutePath),
|
||||
HealthPath: strings.TrimSpace(o.RouterHealthPath),
|
||||
CandidateMappingPath: strings.TrimSpace(o.RouterCandidateMappingPath),
|
||||
CustomizedEnvs: envs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newLLMRouterEnvsFromArgs(args []string) ([]*api.LLMRouterEnv, error) {
|
||||
envs := make([]*api.LLMRouterEnv, 0, len(args))
|
||||
for _, arg := range args {
|
||||
key, val, ok := strings.Cut(arg, "=")
|
||||
key = strings.TrimSpace(key)
|
||||
if !ok || key == "" {
|
||||
return nil, errors.Errorf("invalid --router-env %q, expected key=value", arg)
|
||||
}
|
||||
envs = append(envs, &api.LLMRouterEnv{Key: key, Value: strings.TrimSpace(val)})
|
||||
}
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
func (o *LLMSkuCreateOptions) buildModelSpec() (*api.InstantModelImportInput, error) {
|
||||
if o.ModelTag == "" {
|
||||
return nil, errors.Error("--model-tag is required for model spec")
|
||||
|
||||
Reference in New Issue
Block a user