Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6439ab1515 | |||
| f94226122c | |||
| 7493618fdc | |||
| 205efd40a1 | |||
| 14207f8492 | |||
| 4e850c2834 |
@@ -27,6 +27,7 @@ Built in Go for performance and simplicity, llama-swap has zero dependencies and
|
|||||||
- `v1/images/edits`
|
- `v1/images/edits`
|
||||||
- ✅ Anthropic API supported endpoints:
|
- ✅ Anthropic API supported endpoints:
|
||||||
- `v1/messages`
|
- `v1/messages`
|
||||||
|
- `v1/messages/count_tokens`
|
||||||
- ✅ llama-server (llama.cpp) supported endpoints
|
- ✅ llama-server (llama.cpp) supported endpoints
|
||||||
- `v1/rerank`, `v1/reranking`, `/rerank`
|
- `v1/rerank`, `v1/reranking`, `/rerank`
|
||||||
- `/infill` - for code infilling
|
- `/infill` - for code infilling
|
||||||
|
|||||||
+59
-175
@@ -184,8 +184,16 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
|
yamlStr := string(data)
|
||||||
|
|
||||||
// default configuration values
|
// Phase 1: Substitute all ${env.VAR} macros at string level
|
||||||
|
// This is safe because env values are simple strings without YAML formatting
|
||||||
|
yamlStr, err = substituteEnvMacros(yamlStr)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal into full Config with defaults
|
||||||
config := Config{
|
config := Config{
|
||||||
HealthCheckTimeout: 120,
|
HealthCheckTimeout: 120,
|
||||||
StartPort: 5800,
|
StartPort: 5800,
|
||||||
@@ -194,13 +202,11 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
LogToStdout: LogToStdoutProxy,
|
LogToStdout: LogToStdoutProxy,
|
||||||
MetricsMaxInMemory: 1000,
|
MetricsMaxInMemory: 1000,
|
||||||
}
|
}
|
||||||
err = yaml.Unmarshal(data, &config)
|
if err = yaml.Unmarshal([]byte(yamlStr), &config); err != nil {
|
||||||
if err != nil {
|
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.HealthCheckTimeout < 15 {
|
if config.HealthCheckTimeout < 15 {
|
||||||
// set a minimum of 15 seconds
|
|
||||||
config.HealthCheckTimeout = 15
|
config.HealthCheckTimeout = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,108 +231,46 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* check macro constraint rules:
|
// Validate global macros
|
||||||
|
|
||||||
- name must fit the regex ^[a-zA-Z0-9_-]+$
|
|
||||||
- names must be less than 64 characters (no reason, just cause)
|
|
||||||
- name can not be any reserved macros: PORT, MODEL_ID
|
|
||||||
- macro values must be less than 1024 characters
|
|
||||||
*/
|
|
||||||
for _, macro := range config.Macros {
|
for _, macro := range config.Macros {
|
||||||
if err = validateMacro(macro.Name, macro.Value); err != nil {
|
if err = validateMacro(macro.Name, macro.Value); err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process environment variable macros in global macro values first
|
// Get and sort all model IDs for consistent port assignment
|
||||||
for i, macro := range config.Macros {
|
|
||||||
if strVal, ok := macro.Value.(string); ok {
|
|
||||||
newVal, err := substituteEnvMacros(strVal)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("global macro '%s': %w", macro.Name, err)
|
|
||||||
}
|
|
||||||
config.Macros[i].Value = newVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and sort all model IDs first, makes testing more consistent
|
|
||||||
modelIds := make([]string, 0, len(config.Models))
|
modelIds := make([]string, 0, len(config.Models))
|
||||||
for modelId := range config.Models {
|
for modelId := range config.Models {
|
||||||
modelIds = append(modelIds, modelId)
|
modelIds = append(modelIds, modelId)
|
||||||
}
|
}
|
||||||
sort.Strings(modelIds) // This guarantees stable iteration order
|
sort.Strings(modelIds)
|
||||||
|
|
||||||
nextPort := config.StartPort
|
nextPort := config.StartPort
|
||||||
for _, modelId := range modelIds {
|
for _, modelId := range modelIds {
|
||||||
modelConfig := config.Models[modelId]
|
modelConfig := config.Models[modelId]
|
||||||
|
|
||||||
// Strip comments from command fields before macro expansion
|
// Strip comments from command fields
|
||||||
modelConfig.Cmd = StripComments(modelConfig.Cmd)
|
modelConfig.Cmd = StripComments(modelConfig.Cmd)
|
||||||
modelConfig.CmdStop = StripComments(modelConfig.CmdStop)
|
modelConfig.CmdStop = StripComments(modelConfig.CmdStop)
|
||||||
|
|
||||||
// Substitute environment variable macros in model fields
|
// Validate model macros
|
||||||
modelConfig.Cmd, err = substituteEnvMacros(modelConfig.Cmd)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("model %s cmd: %w", modelId, err)
|
|
||||||
}
|
|
||||||
modelConfig.CmdStop, err = substituteEnvMacros(modelConfig.CmdStop)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("model %s cmdStop: %w", modelId, err)
|
|
||||||
}
|
|
||||||
modelConfig.Proxy, err = substituteEnvMacros(modelConfig.Proxy)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("model %s proxy: %w", modelId, err)
|
|
||||||
}
|
|
||||||
modelConfig.CheckEndpoint, err = substituteEnvMacros(modelConfig.CheckEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("model %s checkEndpoint: %w", modelId, err)
|
|
||||||
}
|
|
||||||
modelConfig.Filters.StripParams, err = substituteEnvMacros(modelConfig.Filters.StripParams)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("model %s filters.stripParams: %w", modelId, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substitute env macros in model-level macro values
|
|
||||||
for i, macro := range modelConfig.Macros {
|
|
||||||
if strVal, ok := macro.Value.(string); ok {
|
|
||||||
newVal, err := substituteEnvMacros(strVal)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("model %s macro '%s': %w", modelId, macro.Name, err)
|
|
||||||
}
|
|
||||||
modelConfig.Macros[i].Value = newVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substitute env macros in metadata
|
|
||||||
if len(modelConfig.Metadata) > 0 {
|
|
||||||
result, err := substituteEnvMacrosInValue(modelConfig.Metadata)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("model %s metadata: %w", modelId, err)
|
|
||||||
}
|
|
||||||
modelConfig.Metadata = result.(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate model macros
|
|
||||||
for _, macro := range modelConfig.Macros {
|
for _, macro := range modelConfig.Macros {
|
||||||
if err = validateMacro(macro.Name, macro.Value); err != nil {
|
if err = validateMacro(macro.Name, macro.Value); err != nil {
|
||||||
return Config{}, fmt.Errorf("model %s: %s", modelId, err.Error())
|
return Config{}, fmt.Errorf("model %s: %s", modelId, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge global config and model macros. Model macros take precedence
|
// Build merged macro list: MODEL_ID + global macros + model macros (model overrides global)
|
||||||
mergedMacros := make(MacroList, 0, len(config.Macros)+len(modelConfig.Macros))
|
mergedMacros := make(MacroList, 0, len(config.Macros)+len(modelConfig.Macros)+1)
|
||||||
mergedMacros = append(mergedMacros, MacroEntry{Name: "MODEL_ID", Value: modelId})
|
mergedMacros = append(mergedMacros, MacroEntry{Name: "MODEL_ID", Value: modelId})
|
||||||
|
|
||||||
// Add global macros first
|
|
||||||
mergedMacros = append(mergedMacros, config.Macros...)
|
mergedMacros = append(mergedMacros, config.Macros...)
|
||||||
|
|
||||||
// Add model macros (can override global)
|
// Add model macros (override globals with same name)
|
||||||
for _, entry := range modelConfig.Macros {
|
for _, entry := range modelConfig.Macros {
|
||||||
// Remove any existing global macro with same name
|
|
||||||
found := false
|
found := false
|
||||||
for i, existing := range mergedMacros {
|
for i, existing := range mergedMacros {
|
||||||
if existing.Name == entry.Name {
|
if existing.Name == entry.Name {
|
||||||
mergedMacros[i] = entry // Override
|
mergedMacros[i] = entry
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -336,23 +280,20 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First pass: Substitute user-defined macros in reverse order (LIFO - last defined first)
|
// Substitute remaining macros in model fields (LIFO order)
|
||||||
// This allows later macros to reference earlier ones
|
|
||||||
for i := len(mergedMacros) - 1; i >= 0; i-- {
|
for i := len(mergedMacros) - 1; i >= 0; i-- {
|
||||||
entry := mergedMacros[i]
|
entry := mergedMacros[i]
|
||||||
macroSlug := fmt.Sprintf("${%s}", entry.Name)
|
macroSlug := fmt.Sprintf("${%s}", entry.Name)
|
||||||
macroStr := fmt.Sprintf("%v", entry.Value)
|
macroStr := fmt.Sprintf("%v", entry.Value)
|
||||||
|
|
||||||
// Substitute in command fields
|
|
||||||
modelConfig.Cmd = strings.ReplaceAll(modelConfig.Cmd, macroSlug, macroStr)
|
modelConfig.Cmd = strings.ReplaceAll(modelConfig.Cmd, macroSlug, macroStr)
|
||||||
modelConfig.CmdStop = strings.ReplaceAll(modelConfig.CmdStop, macroSlug, macroStr)
|
modelConfig.CmdStop = strings.ReplaceAll(modelConfig.CmdStop, macroSlug, macroStr)
|
||||||
modelConfig.Proxy = strings.ReplaceAll(modelConfig.Proxy, macroSlug, macroStr)
|
modelConfig.Proxy = strings.ReplaceAll(modelConfig.Proxy, macroSlug, macroStr)
|
||||||
modelConfig.CheckEndpoint = strings.ReplaceAll(modelConfig.CheckEndpoint, macroSlug, macroStr)
|
modelConfig.CheckEndpoint = strings.ReplaceAll(modelConfig.CheckEndpoint, macroSlug, macroStr)
|
||||||
modelConfig.Filters.StripParams = strings.ReplaceAll(modelConfig.Filters.StripParams, macroSlug, macroStr)
|
modelConfig.Filters.StripParams = strings.ReplaceAll(modelConfig.Filters.StripParams, macroSlug, macroStr)
|
||||||
|
|
||||||
// Substitute in metadata (recursive)
|
// Substitute in metadata (type-preserving)
|
||||||
if len(modelConfig.Metadata) > 0 {
|
if len(modelConfig.Metadata) > 0 {
|
||||||
var err error
|
|
||||||
result, err := substituteMacroInValue(modelConfig.Metadata, entry.Name, entry.Value)
|
result, err := substituteMacroInValue(modelConfig.Metadata, entry.Name, entry.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, fmt.Errorf("model %s metadata: %s", modelId, err.Error())
|
return Config{}, fmt.Errorf("model %s metadata: %s", modelId, err.Error())
|
||||||
@@ -361,18 +302,14 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final pass: check if PORT macro is needed after macro expansion
|
// Handle PORT macro - only allocate if cmd uses it
|
||||||
// ${PORT} is a resource on the local machine so a new port is only allocated
|
|
||||||
// if it is required in either cmd or proxy keys
|
|
||||||
cmdHasPort := strings.Contains(modelConfig.Cmd, "${PORT}")
|
cmdHasPort := strings.Contains(modelConfig.Cmd, "${PORT}")
|
||||||
proxyHasPort := strings.Contains(modelConfig.Proxy, "${PORT}")
|
proxyHasPort := strings.Contains(modelConfig.Proxy, "${PORT}")
|
||||||
if cmdHasPort || proxyHasPort { // either has it
|
if cmdHasPort || proxyHasPort {
|
||||||
if !cmdHasPort && proxyHasPort { // but both don't have it
|
if !cmdHasPort && proxyHasPort {
|
||||||
return Config{}, fmt.Errorf("model %s: proxy uses ${PORT} but cmd does not - ${PORT} is only available when used in cmd", modelId)
|
return Config{}, fmt.Errorf("model %s: proxy uses ${PORT} but cmd does not - ${PORT} is only available when used in cmd", modelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add PORT macro and substitute it
|
|
||||||
portEntry := MacroEntry{Name: "PORT", Value: nextPort}
|
|
||||||
macroSlug := "${PORT}"
|
macroSlug := "${PORT}"
|
||||||
macroStr := fmt.Sprintf("%v", nextPort)
|
macroStr := fmt.Sprintf("%v", nextPort)
|
||||||
|
|
||||||
@@ -380,10 +317,8 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
modelConfig.CmdStop = strings.ReplaceAll(modelConfig.CmdStop, macroSlug, macroStr)
|
modelConfig.CmdStop = strings.ReplaceAll(modelConfig.CmdStop, macroSlug, macroStr)
|
||||||
modelConfig.Proxy = strings.ReplaceAll(modelConfig.Proxy, macroSlug, macroStr)
|
modelConfig.Proxy = strings.ReplaceAll(modelConfig.Proxy, macroSlug, macroStr)
|
||||||
|
|
||||||
// Substitute PORT in metadata
|
|
||||||
if len(modelConfig.Metadata) > 0 {
|
if len(modelConfig.Metadata) > 0 {
|
||||||
var err error
|
result, err := substituteMacroInValue(modelConfig.Metadata, "PORT", nextPort)
|
||||||
result, err := substituteMacroInValue(modelConfig.Metadata, portEntry.Name, portEntry.Value)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, fmt.Errorf("model %s metadata: %s", modelId, err.Error())
|
return Config{}, fmt.Errorf("model %s metadata: %s", modelId, err.Error())
|
||||||
}
|
}
|
||||||
@@ -393,7 +328,7 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
nextPort++
|
nextPort++
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure there are no unknown macros that have not been replaced
|
// Validate no unknown macros remain
|
||||||
fieldMap := map[string]string{
|
fieldMap := map[string]string{
|
||||||
"cmd": modelConfig.Cmd,
|
"cmd": modelConfig.Cmd,
|
||||||
"cmdStop": modelConfig.CmdStop,
|
"cmdStop": modelConfig.CmdStop,
|
||||||
@@ -407,42 +342,27 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
macroName := match[1]
|
macroName := match[1]
|
||||||
if macroName == "PID" && fieldName == "cmdStop" {
|
if macroName == "PID" && fieldName == "cmdStop" {
|
||||||
continue // this is ok, has to be replaced by process later
|
continue // replaced at runtime
|
||||||
}
|
}
|
||||||
// Reserved macros are always valid (they should have been substituted already)
|
|
||||||
if macroName == "PORT" || macroName == "MODEL_ID" {
|
if macroName == "PORT" || macroName == "MODEL_ID" {
|
||||||
return Config{}, fmt.Errorf("macro '${%s}' should have been substituted in %s.%s", macroName, modelId, fieldName)
|
return Config{}, fmt.Errorf("macro '${%s}' should have been substituted in %s.%s", macroName, modelId, fieldName)
|
||||||
}
|
}
|
||||||
// Any other macro is unknown
|
|
||||||
return Config{}, fmt.Errorf("unknown macro '${%s}' found in %s.%s", macroName, modelId, fieldName)
|
return Config{}, fmt.Errorf("unknown macro '${%s}' found in %s.%s", macroName, modelId, fieldName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for unsubstituted env macros
|
|
||||||
envMatches := envMacroRegex.FindAllStringSubmatch(fieldValue, -1)
|
|
||||||
for _, match := range envMatches {
|
|
||||||
varName := match[1]
|
|
||||||
return Config{}, fmt.Errorf("environment variable '%s' not set (found in %s.%s)", varName, modelId, fieldName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for unknown macros in metadata
|
|
||||||
if len(modelConfig.Metadata) > 0 {
|
if len(modelConfig.Metadata) > 0 {
|
||||||
if err := validateNestedForUnknownMacros(modelConfig.Metadata, fmt.Sprintf("model %s metadata", modelId)); err != nil {
|
if err := validateNestedForUnknownMacros(modelConfig.Metadata, fmt.Sprintf("model %s metadata", modelId)); err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the proxy URL.
|
|
||||||
if _, err := url.Parse(modelConfig.Proxy); err != nil {
|
if _, err := url.Parse(modelConfig.Proxy); err != nil {
|
||||||
return Config{}, fmt.Errorf(
|
return Config{}, fmt.Errorf("model %s: invalid proxy URL: %w", modelId, err)
|
||||||
"model %s: invalid proxy URL: %w", modelId, err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if sendLoadingState is nil, set it to the global config value
|
|
||||||
// see #366
|
|
||||||
if modelConfig.SendLoadingState == nil {
|
if modelConfig.SendLoadingState == nil {
|
||||||
v := config.SendLoadingState // copy it
|
v := config.SendLoadingState
|
||||||
modelConfig.SendLoadingState = &v
|
modelConfig.SendLoadingState = &v
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,18 +370,17 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config = AddDefaultGroupToConfig(config)
|
config = AddDefaultGroupToConfig(config)
|
||||||
// check that members are all unique in the groups
|
|
||||||
memberUsage := make(map[string]string) // maps member to group it appears in
|
// Validate group members
|
||||||
|
memberUsage := make(map[string]string)
|
||||||
for groupID, groupConfig := range config.Groups {
|
for groupID, groupConfig := range config.Groups {
|
||||||
prevSet := make(map[string]bool)
|
prevSet := make(map[string]bool)
|
||||||
for _, member := range groupConfig.Members {
|
for _, member := range groupConfig.Members {
|
||||||
// Check for duplicates within this group
|
|
||||||
if _, found := prevSet[member]; found {
|
if _, found := prevSet[member]; found {
|
||||||
return Config{}, fmt.Errorf("duplicate model member %s found in group: %s", member, groupID)
|
return Config{}, fmt.Errorf("duplicate model member %s found in group: %s", member, groupID)
|
||||||
}
|
}
|
||||||
prevSet[member] = true
|
prevSet[member] = true
|
||||||
|
|
||||||
// Check if member is used in another group
|
|
||||||
if existingGroup, exists := memberUsage[member]; exists {
|
if existingGroup, exists := memberUsage[member]; exists {
|
||||||
return Config{}, fmt.Errorf("model member %s is used in multiple groups: %s and %s", member, existingGroup, groupID)
|
return Config{}, fmt.Errorf("model member %s is used in multiple groups: %s and %s", member, existingGroup, groupID)
|
||||||
}
|
}
|
||||||
@@ -469,7 +388,7 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up hooks preload
|
// Clean up hooks preload
|
||||||
if len(config.Hooks.OnStartup.Preload) > 0 {
|
if len(config.Hooks.OnStartup.Preload) > 0 {
|
||||||
var toPreload []string
|
var toPreload []string
|
||||||
for _, modelID := range config.Hooks.OnStartup.Preload {
|
for _, modelID := range config.Hooks.OnStartup.Preload {
|
||||||
@@ -481,30 +400,23 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
toPreload = append(toPreload, real)
|
toPreload = append(toPreload, real)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Hooks.OnStartup.Preload = toPreload
|
config.Hooks.OnStartup.Preload = toPreload
|
||||||
}
|
}
|
||||||
|
|
||||||
// check api keys validity and substitute env macros
|
// Validate API keys (env macros already substituted at string level)
|
||||||
for i, apikey := range config.RequiredAPIKeys {
|
for i, apikey := range config.RequiredAPIKeys {
|
||||||
apikey, err = substituteEnvMacros(apikey)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("apiKeys[%d]: %w", i, err)
|
|
||||||
}
|
|
||||||
config.RequiredAPIKeys[i] = apikey
|
|
||||||
|
|
||||||
if apikey == "" {
|
if apikey == "" {
|
||||||
return Config{}, fmt.Errorf("empty api key found in apiKeys")
|
return Config{}, fmt.Errorf("empty api key found in apiKeys")
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(apikey, " ") {
|
if strings.Contains(apikey, " ") {
|
||||||
return Config{}, fmt.Errorf("api key cannot contain spaces: `%s`", apikey)
|
return Config{}, fmt.Errorf("api key cannot contain spaces: `%s`", apikey)
|
||||||
}
|
}
|
||||||
|
config.RequiredAPIKeys[i] = apikey
|
||||||
}
|
}
|
||||||
|
|
||||||
// substitute macros and env macros in peer fields
|
// Process peers with global macro substitution
|
||||||
for peerName, peerConfig := range config.Peers {
|
for peerName, peerConfig := range config.Peers {
|
||||||
// Substitute global macros first (LIFO order like models)
|
// Substitute global macros (LIFO order)
|
||||||
for i := len(config.Macros) - 1; i >= 0; i-- {
|
for i := len(config.Macros) - 1; i >= 0; i-- {
|
||||||
entry := config.Macros[i]
|
entry := config.Macros[i]
|
||||||
macroSlug := fmt.Sprintf("${%s}", entry.Name)
|
macroSlug := fmt.Sprintf("${%s}", entry.Name)
|
||||||
@@ -513,7 +425,7 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
peerConfig.ApiKey = strings.ReplaceAll(peerConfig.ApiKey, macroSlug, macroStr)
|
peerConfig.ApiKey = strings.ReplaceAll(peerConfig.ApiKey, macroSlug, macroStr)
|
||||||
peerConfig.Filters.StripParams = strings.ReplaceAll(peerConfig.Filters.StripParams, macroSlug, macroStr)
|
peerConfig.Filters.StripParams = strings.ReplaceAll(peerConfig.Filters.StripParams, macroSlug, macroStr)
|
||||||
|
|
||||||
// Substitute in setParams
|
// Substitute in setParams (type-preserving)
|
||||||
if len(peerConfig.Filters.SetParams) > 0 {
|
if len(peerConfig.Filters.SetParams) > 0 {
|
||||||
result, err := substituteMacroInValue(peerConfig.Filters.SetParams, entry.Name, entry.Value)
|
result, err := substituteMacroInValue(peerConfig.Filters.SetParams, entry.Name, entry.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -523,25 +435,6 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Substitute env macros
|
|
||||||
peerConfig.ApiKey, err = substituteEnvMacros(peerConfig.ApiKey)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("peers.%s.apiKey: %w", peerName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
peerConfig.Filters.StripParams, err = substituteEnvMacros(peerConfig.Filters.StripParams)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("peers.%s.filters.stripParams: %w", peerName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(peerConfig.Filters.SetParams) > 0 {
|
|
||||||
result, err := substituteEnvMacrosInValue(peerConfig.Filters.SetParams)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("peers.%s.filters.setParams: %w", peerName, err)
|
|
||||||
}
|
|
||||||
peerConfig.Filters.SetParams = result.(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate no unknown macros remain
|
// Validate no unknown macros remain
|
||||||
if matches := macroPatternRegex.FindAllStringSubmatch(peerConfig.ApiKey, -1); len(matches) > 0 {
|
if matches := macroPatternRegex.FindAllStringSubmatch(peerConfig.ApiKey, -1); len(matches) > 0 {
|
||||||
return Config{}, fmt.Errorf("peers.%s.apiKey: unknown macro '${%s}'", peerName, matches[0][1])
|
return Config{}, fmt.Errorf("peers.%s.apiKey: unknown macro '${%s}'", peerName, matches[0][1])
|
||||||
@@ -554,7 +447,6 @@ func LoadConfigFromReader(r io.Reader) (Config, error) {
|
|||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Peers[peerName] = peerConfig
|
config.Peers[peerName] = peerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,7 +668,7 @@ func substituteMacroInValue(value any, macroName string, macroValue any) (any, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// substituteEnvMacros replaces ${env.VAR_NAME} with environment variable values
|
// substituteEnvMacros replaces ${env.VAR_NAME} with environment variable values
|
||||||
// Returns error if any env var is not set
|
// Returns error if any env var is not set or contains invalid characters
|
||||||
func substituteEnvMacros(s string) (string, error) {
|
func substituteEnvMacros(s string) (string, error) {
|
||||||
result := s
|
result := s
|
||||||
matches := envMacroRegex.FindAllStringSubmatch(s, -1)
|
matches := envMacroRegex.FindAllStringSubmatch(s, -1)
|
||||||
@@ -788,40 +680,32 @@ func substituteEnvMacros(s string) (string, error) {
|
|||||||
if !exists {
|
if !exists {
|
||||||
return "", fmt.Errorf("environment variable '%s' is not set", varName)
|
return "", fmt.Errorf("environment variable '%s' is not set", varName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize the value for safe YAML substitution
|
||||||
|
value, err := sanitizeEnvValueForYAML(value, varName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
result = strings.ReplaceAll(result, fullMatch, value)
|
result = strings.ReplaceAll(result, fullMatch, value)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// substituteEnvMacrosInValue recursively substitutes env macros in nested structures
|
// sanitizeEnvValueForYAML ensures an environment variable value is safe for YAML substitution.
|
||||||
func substituteEnvMacrosInValue(value any) (any, error) {
|
// It rejects values with characters that break YAML structure and escapes quotes/backslashes
|
||||||
switch v := value.(type) {
|
// for compatibility with double-quoted YAML strings.
|
||||||
case string:
|
func sanitizeEnvValueForYAML(value, varName string) (string, error) {
|
||||||
return substituteEnvMacros(v)
|
// Reject values that would break YAML structure regardless of quoting context
|
||||||
|
if strings.ContainsAny(value, "\n\r\x00") {
|
||||||
|
return "", fmt.Errorf("environment variable '%s' contains newlines or null bytes which are not allowed in YAML substitution", varName)
|
||||||
|
}
|
||||||
|
|
||||||
case map[string]any:
|
// Escape backslashes and double quotes for safe use in double-quoted YAML strings.
|
||||||
newMap := make(map[string]any)
|
// In unquoted contexts, these escapes appear literally (harmless for most use cases).
|
||||||
for key, val := range v {
|
// In double-quoted contexts, they are interpreted correctly.
|
||||||
newVal, err := substituteEnvMacrosInValue(val)
|
value = strings.ReplaceAll(value, `\`, `\\`)
|
||||||
if err != nil {
|
value = strings.ReplaceAll(value, `"`, `\"`)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newMap[key] = newVal
|
|
||||||
}
|
|
||||||
return newMap, nil
|
|
||||||
|
|
||||||
case []any:
|
|
||||||
newSlice := make([]any, len(v))
|
|
||||||
for i, val := range v {
|
|
||||||
newVal, err := substituteEnvMacrosInValue(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newSlice[i] = newVal
|
|
||||||
}
|
|
||||||
return newSlice, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -834,7 +834,7 @@ func TestConfig_APIKeys_EnvMacros(t *testing.T) {
|
|||||||
content := `apiKeys: ["${env.NONEXISTENT_API_KEY}"]`
|
content := `apiKeys: ["${env.NONEXISTENT_API_KEY}"]`
|
||||||
_, err := LoadConfigFromReader(strings.NewReader(content))
|
_, err := LoadConfigFromReader(strings.NewReader(content))
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "apiKeys[0]")
|
// With string-level env substitution, error only includes var name
|
||||||
assert.Contains(t, err.Error(), "NONEXISTENT_API_KEY")
|
assert.Contains(t, err.Error(), "NONEXISTENT_API_KEY")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1056,6 +1056,70 @@ models:
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "server --auth admin:secret", config.Models["test"].Cmd)
|
assert.Equal(t, "server --auth admin:secret", config.Models["test"].Cmd)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("env value with newline is rejected", func(t *testing.T) {
|
||||||
|
t.Setenv("TEST_MULTILINE", "line1\nline2")
|
||||||
|
|
||||||
|
content := `
|
||||||
|
models:
|
||||||
|
test:
|
||||||
|
cmd: "server --config ${env.TEST_MULTILINE}"
|
||||||
|
proxy: "http://localhost:8080"
|
||||||
|
`
|
||||||
|
_, err := LoadConfigFromReader(strings.NewReader(content))
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "TEST_MULTILINE")
|
||||||
|
assert.Contains(t, err.Error(), "newlines")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("env value with carriage return is rejected", func(t *testing.T) {
|
||||||
|
t.Setenv("TEST_CR", "line1\rline2")
|
||||||
|
|
||||||
|
content := `
|
||||||
|
models:
|
||||||
|
test:
|
||||||
|
cmd: "server --config ${env.TEST_CR}"
|
||||||
|
proxy: "http://localhost:8080"
|
||||||
|
`
|
||||||
|
_, err := LoadConfigFromReader(strings.NewReader(content))
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "TEST_CR")
|
||||||
|
assert.Contains(t, err.Error(), "newlines")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("env value with quotes is escaped for YAML", func(t *testing.T) {
|
||||||
|
t.Setenv("TEST_QUOTED", `value with "quotes"`)
|
||||||
|
|
||||||
|
content := `
|
||||||
|
models:
|
||||||
|
test:
|
||||||
|
cmd: "server --arg \"${env.TEST_QUOTED}\""
|
||||||
|
proxy: "http://localhost:8080"
|
||||||
|
`
|
||||||
|
config, err := LoadConfigFromReader(strings.NewReader(content))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// Quotes are escaped before YAML parsing, then YAML unescapes them
|
||||||
|
// Final result preserves the original value with quotes
|
||||||
|
assert.Contains(t, config.Models["test"].Cmd, `"quotes"`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("env value with backslash is escaped for YAML", func(t *testing.T) {
|
||||||
|
t.Setenv("TEST_BACKSLASH", `path\to\file`)
|
||||||
|
|
||||||
|
content := `
|
||||||
|
models:
|
||||||
|
test:
|
||||||
|
cmd: "server --path \"${env.TEST_BACKSLASH}\""
|
||||||
|
proxy: "http://localhost:8080"
|
||||||
|
`
|
||||||
|
config, err := LoadConfigFromReader(strings.NewReader(content))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// Backslashes are escaped before YAML parsing, then YAML unescapes them
|
||||||
|
// Final result preserves the original value with backslashes
|
||||||
|
assert.Contains(t, config.Models["test"].Cmd, `path\to\file`)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_PeerApiKey_EnvMacros(t *testing.T) {
|
func TestConfig_PeerApiKey_EnvMacros(t *testing.T) {
|
||||||
@@ -1086,7 +1150,7 @@ peers:
|
|||||||
`
|
`
|
||||||
_, err := LoadConfigFromReader(strings.NewReader(content))
|
_, err := LoadConfigFromReader(strings.NewReader(content))
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "peers.openrouter.apiKey")
|
// With string-level env substitution, error only includes var name
|
||||||
assert.Contains(t, err.Error(), "NONEXISTENT_PEER_KEY")
|
assert.Contains(t, err.Error(), "NONEXISTENT_PEER_KEY")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -282,6 +282,8 @@ func (pm *ProxyManager) setupGinEngine() {
|
|||||||
pm.ginEngine.POST("/v1/completions", pm.apiKeyAuth(), pm.proxyInferenceHandler)
|
pm.ginEngine.POST("/v1/completions", pm.apiKeyAuth(), pm.proxyInferenceHandler)
|
||||||
// Support anthropic /v1/messages (added https://github.com/ggml-org/llama.cpp/pull/17570)
|
// Support anthropic /v1/messages (added https://github.com/ggml-org/llama.cpp/pull/17570)
|
||||||
pm.ginEngine.POST("/v1/messages", pm.apiKeyAuth(), pm.proxyInferenceHandler)
|
pm.ginEngine.POST("/v1/messages", pm.apiKeyAuth(), pm.proxyInferenceHandler)
|
||||||
|
// Support anthropic count_tokens API (Also added in the above PR)
|
||||||
|
pm.ginEngine.POST("/v1/messages/count_tokens", pm.apiKeyAuth(), pm.proxyInferenceHandler)
|
||||||
|
|
||||||
// Support embeddings and reranking
|
// Support embeddings and reranking
|
||||||
pm.ginEngine.POST("/v1/embeddings", pm.apiKeyAuth(), pm.proxyInferenceHandler)
|
pm.ginEngine.POST("/v1/embeddings", pm.apiKeyAuth(), pm.proxyInferenceHandler)
|
||||||
@@ -930,6 +932,11 @@ func (pm *ProxyManager) listRunningProcessesHandler(context *gin.Context) {
|
|||||||
runningProcesses = append(runningProcesses, gin.H{
|
runningProcesses = append(runningProcesses, gin.H{
|
||||||
"model": process.ID,
|
"model": process.ID,
|
||||||
"state": process.state,
|
"state": process.state,
|
||||||
|
"cmd": process.config.Cmd,
|
||||||
|
"proxy": process.config.Proxy,
|
||||||
|
"ttl": process.config.UnloadAfter,
|
||||||
|
"name": process.config.Name,
|
||||||
|
"description": process.config.Description,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -674,6 +674,11 @@ func TestProxyManager_RunningEndpoint(t *testing.T) {
|
|||||||
Running []struct {
|
Running []struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
|
Cmd string `json:"cmd"`
|
||||||
|
Proxy string `json:"proxy"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
} `json:"running"`
|
} `json:"running"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,6 +726,11 @@ func TestProxyManager_RunningEndpoint(t *testing.T) {
|
|||||||
|
|
||||||
// Is the model loaded?
|
// Is the model loaded?
|
||||||
assert.Equal(t, "ready", response.Running[0].State)
|
assert.Equal(t, "ready", response.Running[0].State)
|
||||||
|
|
||||||
|
// Verify extended fields are present
|
||||||
|
assert.NotEmpty(t, response.Running[0].Cmd, "cmd should be populated")
|
||||||
|
assert.NotEmpty(t, response.Running[0].Proxy, "proxy should be populated")
|
||||||
|
assert.Equal(t, 0, response.Running[0].TTL, "ttl should default to 0")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+80
-25
@@ -75,6 +75,7 @@
|
|||||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
@@ -1593,6 +1594,66 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.0.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "0.2.10",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "^1.4.3",
|
||||||
|
"@emnapi/runtime": "^1.4.3",
|
||||||
|
"@tybys/wasm-util": "^0.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||||
|
"version": "2.8.0",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.1.8",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz",
|
||||||
@@ -1707,6 +1768,7 @@
|
|||||||
"integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==",
|
"integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -1767,6 +1829,7 @@
|
|||||||
"integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==",
|
"integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.33.1",
|
"@typescript-eslint/scope-manager": "8.33.1",
|
||||||
"@typescript-eslint/types": "8.33.1",
|
"@typescript-eslint/types": "8.33.1",
|
||||||
@@ -2018,6 +2081,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2126,6 +2190,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001718",
|
"caniuse-lite": "^1.0.30001718",
|
||||||
"electron-to-chromium": "^1.5.160",
|
"electron-to-chromium": "^1.5.160",
|
||||||
@@ -2392,6 +2457,7 @@
|
|||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3271,9 +3337,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minizlib": {
|
"node_modules/minizlib": {
|
||||||
"version": "3.0.2",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||||
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
|
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3283,22 +3349,6 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"mkdirp": "dist/cjs/src/bin.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -3517,6 +3567,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3526,6 +3577,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -3791,17 +3843,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.4.3",
|
"version": "7.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz",
|
||||||
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
|
"integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/fs-minipass": "^4.0.0",
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
"chownr": "^3.0.0",
|
"chownr": "^3.0.0",
|
||||||
"minipass": "^7.1.2",
|
"minipass": "^7.1.2",
|
||||||
"minizlib": "^3.0.1",
|
"minizlib": "^3.1.0",
|
||||||
"mkdirp": "^3.0.1",
|
|
||||||
"yallist": "^5.0.0"
|
"yallist": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3856,6 +3907,7 @@
|
|||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -3908,6 +3960,7 @@
|
|||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -3986,6 +4039,7 @@
|
|||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -4076,6 +4130,7 @@
|
|||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user