diff --git a/config.example.yaml b/config.example.yaml index 39f74db2..d8282fc1 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -386,7 +386,8 @@ peers: # - optional, default: "" # - if blank, no key will be added to the request # - key will be injected into headers: Authorization: Bearer and x-api-key: - apiKey: sk-your-openrouter-key + # - can be a string or a macro + apiKey: ${env.OPENROUTER_API_KEY} models: - meta-llama/llama-3.1-8b-instruct - qwen/qwen3-235b-a22b-2507 @@ -413,4 +414,4 @@ peers: # Example: enforce zero-data-retention for OpenRouter provider: data_collection: "deny" - allow_fallbacks: false + zdr: true diff --git a/proxy/config/config.go b/proxy/config/config.go index 99635e08..019519a1 100644 --- a/proxy/config/config.go +++ b/proxy/config/config.go @@ -427,7 +427,7 @@ func LoadConfigFromReader(r io.Reader) (Config, error) { // Check for unknown macros in metadata if len(modelConfig.Metadata) > 0 { - if err := validateMetadataForUnknownMacros(modelConfig.Metadata, modelId); err != nil { + if err := validateNestedForUnknownMacros(modelConfig.Metadata, fmt.Sprintf("model %s metadata", modelId)); err != nil { return Config{}, err } } @@ -502,6 +502,62 @@ func LoadConfigFromReader(r io.Reader) (Config, error) { } } + // substitute macros and env macros in peer fields + for peerName, peerConfig := range config.Peers { + // Substitute global macros first (LIFO order like models) + for i := len(config.Macros) - 1; i >= 0; i-- { + entry := config.Macros[i] + macroSlug := fmt.Sprintf("${%s}", entry.Name) + macroStr := fmt.Sprintf("%v", entry.Value) + + peerConfig.ApiKey = strings.ReplaceAll(peerConfig.ApiKey, macroSlug, macroStr) + peerConfig.Filters.StripParams = strings.ReplaceAll(peerConfig.Filters.StripParams, macroSlug, macroStr) + + // Substitute in setParams + if len(peerConfig.Filters.SetParams) > 0 { + result, err := substituteMacroInValue(peerConfig.Filters.SetParams, entry.Name, entry.Value) + if err != nil { + return Config{}, fmt.Errorf("peers.%s.filters.setParams: %w", peerName, err) + } + peerConfig.Filters.SetParams = result.(map[string]any) + } + } + + // 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 + if matches := macroPatternRegex.FindAllStringSubmatch(peerConfig.ApiKey, -1); len(matches) > 0 { + return Config{}, fmt.Errorf("peers.%s.apiKey: unknown macro '${%s}'", peerName, matches[0][1]) + } + if matches := macroPatternRegex.FindAllStringSubmatch(peerConfig.Filters.StripParams, -1); len(matches) > 0 { + return Config{}, fmt.Errorf("peers.%s.filters.stripParams: unknown macro '${%s}'", peerName, matches[0][1]) + } + if len(peerConfig.Filters.SetParams) > 0 { + if err := validateNestedForUnknownMacros(peerConfig.Filters.SetParams, fmt.Sprintf("peers.%s.filters.setParams", peerName)); err != nil { + return Config{}, err + } + } + + config.Peers[peerName] = peerConfig + } + return config, nil } @@ -632,26 +688,26 @@ func validateMacro(name string, value any) error { return nil } -// validateMetadataForUnknownMacros recursively checks for any remaining macro references in metadata -func validateMetadataForUnknownMacros(value any, modelId string) error { +// validateNestedForUnknownMacros recursively checks for any remaining macro references in nested structures +func validateNestedForUnknownMacros(value any, context string) error { switch v := value.(type) { case string: matches := macroPatternRegex.FindAllStringSubmatch(v, -1) for _, match := range matches { macroName := match[1] - return fmt.Errorf("model %s metadata: unknown macro '${%s}'", modelId, macroName) + return fmt.Errorf("%s: unknown macro '${%s}'", context, macroName) } // Check for unsubstituted env macros envMatches := envMacroRegex.FindAllStringSubmatch(v, -1) for _, match := range envMatches { varName := match[1] - return fmt.Errorf("model %s metadata: environment variable '%s' not set", modelId, varName) + return fmt.Errorf("%s: environment variable '%s' not set", context, varName) } return nil case map[string]any: for _, val := range v { - if err := validateMetadataForUnknownMacros(val, modelId); err != nil { + if err := validateNestedForUnknownMacros(val, context); err != nil { return err } } @@ -659,7 +715,7 @@ func validateMetadataForUnknownMacros(value any, modelId string) error { case []any: for _, val := range v { - if err := validateMetadataForUnknownMacros(val, modelId); err != nil { + if err := validateNestedForUnknownMacros(val, context); err != nil { return err } } diff --git a/proxy/config/config_test.go b/proxy/config/config_test.go index fb574a14..c77b3a78 100644 --- a/proxy/config/config_test.go +++ b/proxy/config/config_test.go @@ -1057,3 +1057,191 @@ models: assert.Equal(t, "server --auth admin:secret", config.Models["test"].Cmd) }) } + +func TestConfig_PeerApiKey_EnvMacros(t *testing.T) { + t.Run("env substitution in peer apiKey", func(t *testing.T) { + t.Setenv("TEST_PEER_API_KEY", "sk-peer-secret-123") + + content := ` +peers: + openrouter: + proxy: https://openrouter.ai/api + apiKey: "${env.TEST_PEER_API_KEY}" + models: + - llama-3.1-8b +` + config, err := LoadConfigFromReader(strings.NewReader(content)) + assert.NoError(t, err) + assert.Equal(t, "sk-peer-secret-123", config.Peers["openrouter"].ApiKey) + }) + + t.Run("missing env var in peer apiKey", func(t *testing.T) { + content := ` +peers: + openrouter: + proxy: https://openrouter.ai/api + apiKey: "${env.NONEXISTENT_PEER_KEY}" + models: + - llama-3.1-8b +` + _, err := LoadConfigFromReader(strings.NewReader(content)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "peers.openrouter.apiKey") + assert.Contains(t, err.Error(), "NONEXISTENT_PEER_KEY") + }) + + t.Run("static apiKey unchanged", func(t *testing.T) { + content := ` +peers: + openrouter: + proxy: https://openrouter.ai/api + apiKey: sk-static-key + models: + - llama-3.1-8b +` + config, err := LoadConfigFromReader(strings.NewReader(content)) + assert.NoError(t, err) + assert.Equal(t, "sk-static-key", config.Peers["openrouter"].ApiKey) + }) + + t.Run("multiple peers with env apiKeys", func(t *testing.T) { + t.Setenv("TEST_PEER_KEY_1", "key-one") + t.Setenv("TEST_PEER_KEY_2", "key-two") + + content := ` +peers: + peer1: + proxy: https://peer1.example.com + apiKey: "${env.TEST_PEER_KEY_1}" + models: + - model-a + peer2: + proxy: https://peer2.example.com + apiKey: "${env.TEST_PEER_KEY_2}" + models: + - model-b +` + config, err := LoadConfigFromReader(strings.NewReader(content)) + assert.NoError(t, err) + assert.Equal(t, "key-one", config.Peers["peer1"].ApiKey) + assert.Equal(t, "key-two", config.Peers["peer2"].ApiKey) + }) + + t.Run("global macro substitution in peer apiKey", func(t *testing.T) { + content := ` +macros: + API_KEY: sk-from-global-macro +peers: + openrouter: + proxy: https://openrouter.ai/api + apiKey: "${API_KEY}" + models: + - llama-3.1-8b +` + config, err := LoadConfigFromReader(strings.NewReader(content)) + assert.NoError(t, err) + assert.Equal(t, "sk-from-global-macro", config.Peers["openrouter"].ApiKey) + }) + + t.Run("global macro in peer filters.stripParams", func(t *testing.T) { + content := ` +macros: + STRIP_LIST: "temperature, top_p" +peers: + openrouter: + proxy: https://openrouter.ai/api + models: + - llama-3.1-8b + filters: + stripParams: "${STRIP_LIST}" +` + config, err := LoadConfigFromReader(strings.NewReader(content)) + assert.NoError(t, err) + assert.Equal(t, "temperature, top_p", config.Peers["openrouter"].Filters.StripParams) + }) + + t.Run("global macro in peer filters.setParams", func(t *testing.T) { + content := ` +macros: + MAX_TOKENS: 4096 +peers: + openrouter: + proxy: https://openrouter.ai/api + models: + - llama-3.1-8b + filters: + setParams: + max_tokens: "${MAX_TOKENS}" +` + config, err := LoadConfigFromReader(strings.NewReader(content)) + assert.NoError(t, err) + assert.Equal(t, 4096, config.Peers["openrouter"].Filters.SetParams["max_tokens"]) + }) + + t.Run("env macro in peer filters.setParams", func(t *testing.T) { + t.Setenv("TEST_RETENTION_POLICY", "deny") + + content := ` +peers: + openrouter: + proxy: https://openrouter.ai/api + models: + - llama-3.1-8b + filters: + setParams: + data_collection: "${env.TEST_RETENTION_POLICY}" +` + config, err := LoadConfigFromReader(strings.NewReader(content)) + assert.NoError(t, err) + assert.Equal(t, "deny", config.Peers["openrouter"].Filters.SetParams["data_collection"]) + }) + + t.Run("env macro in peer filters.stripParams", func(t *testing.T) { + t.Setenv("TEST_STRIP_PARAMS", "frequency_penalty, presence_penalty") + + content := ` +peers: + openrouter: + proxy: https://openrouter.ai/api + models: + - llama-3.1-8b + filters: + stripParams: "${env.TEST_STRIP_PARAMS}" +` + config, err := LoadConfigFromReader(strings.NewReader(content)) + assert.NoError(t, err) + assert.Equal(t, "frequency_penalty, presence_penalty", config.Peers["openrouter"].Filters.StripParams) + }) + + t.Run("unknown macro in peer apiKey fails", func(t *testing.T) { + content := ` +peers: + openrouter: + proxy: https://openrouter.ai/api + apiKey: "${UNDEFINED_MACRO}" + models: + - llama-3.1-8b +` + _, err := LoadConfigFromReader(strings.NewReader(content)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "peers.openrouter.apiKey") + assert.Contains(t, err.Error(), "unknown macro") + }) + + t.Run("unknown macro in peer filters.setParams fails", func(t *testing.T) { + content := ` +peers: + openrouter: + proxy: https://openrouter.ai/api + models: + - llama-3.1-8b + filters: + setParams: + value: "${UNDEFINED_MACRO}" +` + _, err := LoadConfigFromReader(strings.NewReader(content)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "peers.openrouter.filters.setParams") + assert.Contains(t, err.Error(), "unknown macro") + }) +}