Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ type Config struct {
// OpenRouter-specific (optional, improves rate limits)
OpenRouterAppName string
OpenRouterAppURL string

// OpenRouter provider routing - pins requests to specific upstream provider(s)
// so OpenRouter uses your BYOK key and fails hard instead of falling back to a
// credit-billed provider (e.g. Bedrock/Vertex). Set via OPENROUTER_PROVIDER_ONLY
// (comma-separated provider slugs, e.g. "google-ai-studio"). When set, the proxy
// injects {"provider": {"only": [...], "allow_fallback": false}} into the request body.
OpenRouterProviderOnly []string
}

// Load reads configuration from environment variables
Expand Down Expand Up @@ -120,6 +127,9 @@ func Load() (*Config, error) {
// OpenRouter-specific (optional)
OpenRouterAppName: os.Getenv("OPENROUTER_APP_NAME"),
OpenRouterAppURL: os.Getenv("OPENROUTER_APP_URL"),

// OpenRouter provider pinning (optional). Comma-separated provider slugs.
OpenRouterProviderOnly: parseProviderOnly(os.Getenv("OPENROUTER_PROVIDER_ONLY")),
}

// Validate required fields
Expand Down Expand Up @@ -160,6 +170,26 @@ func getEnvAsBoolOrDefault(key string, defaultValue bool) bool {
return defaultValue
}

// parseProviderOnly splits a comma-separated list of OpenRouter provider slugs
// (e.g. "google-ai-studio") into a clean slice, trimming whitespace and dropping
// empties. Returns nil when unset, so the proxy skips provider injection entirely.
func parseProviderOnly(value string) []string {
if value == "" {
return nil
}
parts := strings.Split(value, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
if slug := strings.TrimSpace(p); slug != "" {
result = append(result, slug)
}
}
if len(result) == 0 {
return nil
}
return result
}

// DetectProvider identifies the provider type based on base URL
func (c *Config) DetectProvider() ProviderType {
baseURL := strings.ToLower(c.OpenAIBaseURL)
Expand Down
13 changes: 13 additions & 0 deletions internal/converter/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,19 @@ func ConvertRequest(claudeReq models.ClaudeRequest, cfg *config.Config) (*models
}
}

// Pin provider when configured: forces OpenRouter to use the user's BYOK key for
// the named provider(s) and fail hard instead of falling back to a credit-billed
// upstream (e.g. Bedrock/Vertex). Also keeps routing stable so upstream prefix
// caches stay warm. Applies to both streaming and non-streaming requests.
if cfg.DetectProvider() == config.ProviderOpenRouter && len(cfg.OpenRouterProviderOnly) > 0 {
// "only" pins routing to the named provider(s) and fails hard if none can
// serve the request, rather than falling back to a credit-billed upstream.
// (OpenRouter rejects an "allow_fallback" key here; "only" already prevents fallback.)
openaiReq.Provider = map[string]interface{}{
"only": cfg.OpenRouterProviderOnly,
}
}

// Set token limit using adaptive per-model detection
if claudeReq.MaxTokens > 0 {
// Use capability-based detection - NO hardcoded model patterns!
Expand Down
3 changes: 2 additions & 1 deletion pkg/models/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ type OpenAIRequest struct {
Reasoning map[string]interface{} `json:"reasoning,omitempty"` // OpenRouter reasoning tokens
ReasoningEffort string `json:"reasoning_effort,omitempty"` // OpenAI Chat Completions reasoning (GPT-5 models)
Tools []OpenAITool `json:"tools,omitempty"`
ToolChoice interface{} `json:"tool_choice,omitempty"` // Force tool usage: "auto", "required", or specific tool
ToolChoice interface{} `json:"tool_choice,omitempty"` // Force tool usage: "auto", "required", or specific tool
Provider map[string]interface{} `json:"provider,omitempty"` // OpenRouter provider routing (e.g. {"only": ["google-ai-studio"], "allow_fallback": false})
}

// OpenAITool represents a tool in OpenAI format
Expand Down