mirror of
https://github.com/aljazceru/ollama-free-model-proxy.git
synced 2025-12-17 21:24:18 +01:00
tools
This commit is contained in:
16
README.md
16
README.md
@@ -8,6 +8,7 @@ This is heavily vibecoded and may not be production-ready. It is intended for pe
|
|||||||
## Features
|
## Features
|
||||||
- **Free Mode (Default)**: Automatically selects and uses free models from OpenRouter with intelligent fallback. Enabled by default unless `FREE_MODE=false` is set.
|
- **Free Mode (Default)**: Automatically selects and uses free models from OpenRouter with intelligent fallback. Enabled by default unless `FREE_MODE=false` is set.
|
||||||
- **Model Filtering**: Create a `models-filter/filter` file with model name patterns (one per line). Supports partial matching - `gemini` matches `gemini-2.0-flash-exp:free`. Works in both free and non-free modes.
|
- **Model Filtering**: Create a `models-filter/filter` file with model name patterns (one per line). Supports partial matching - `gemini` matches `gemini-2.0-flash-exp:free`. Works in both free and non-free modes.
|
||||||
|
- **Tool Use Filtering**: Filter for only free models that support function calling/tool use by setting `TOOL_USE_ONLY=true`. Models are filtered based on their `supported_parameters` containing "tools" or "tool_choice".
|
||||||
- **Ollama-like API**: The server listens on `11434` and exposes endpoints similar to Ollama (e.g., `/api/chat`, `/api/tags`).
|
- **Ollama-like API**: The server listens on `11434` and exposes endpoints similar to Ollama (e.g., `/api/chat`, `/api/tags`).
|
||||||
- **Model Listing**: Fetch a list of available models from OpenRouter.
|
- **Model Listing**: Fetch a list of available models from OpenRouter.
|
||||||
- **Model Details**: Retrieve metadata about a specific model.
|
- **Model Details**: Retrieve metadata about a specific model.
|
||||||
@@ -34,6 +35,11 @@ The proxy operates in **free mode** by default, automatically selecting from ava
|
|||||||
export OPENAI_API_KEY="your-openrouter-api-key"
|
export OPENAI_API_KEY="your-openrouter-api-key"
|
||||||
./ollama-proxy
|
./ollama-proxy
|
||||||
|
|
||||||
|
# To only use free models that support tool use/function calling
|
||||||
|
export TOOL_USE_ONLY=true
|
||||||
|
export OPENAI_API_KEY="your-openrouter-api-key"
|
||||||
|
./ollama-proxy
|
||||||
|
|
||||||
#### How Free Mode Works
|
#### How Free Mode Works
|
||||||
|
|
||||||
- **Automatic Model Discovery**: Fetches and caches available free models from OpenRouter
|
- **Automatic Model Discovery**: Fetches and caches available free models from OpenRouter
|
||||||
@@ -128,6 +134,7 @@ curl -X POST http://localhost:11434/v1/chat/completions \
|
|||||||
```bash
|
```bash
|
||||||
OPENAI_API_KEY=your-openrouter-api-key
|
OPENAI_API_KEY=your-openrouter-api-key
|
||||||
FREE_MODE=true
|
FREE_MODE=true
|
||||||
|
TOOL_USE_ONLY=false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -136,7 +143,11 @@ curl -X POST http://localhost:11434/v1/chat/completions \
|
|||||||
mkdir -p models-filter
|
mkdir -p models-filter
|
||||||
echo "gemini" > models-filter/filter # Only show Gemini models
|
echo "gemini" > models-filter/filter # Only show Gemini models
|
||||||
```
|
```
|
||||||
4. **Run with Docker Compose**:
|
|
||||||
|
4. **Optional: Enable tool use filtering**:
|
||||||
|
Set `TOOL_USE_ONLY=true` in your `.env` file to only use models that support function calling/tool use. This filters models based on their `supported_parameters` containing "tools" or "tool_choice".
|
||||||
|
|
||||||
|
5. **Run with Docker Compose**:
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
@@ -148,6 +159,9 @@ The service will be available at `http://localhost:11434`.
|
|||||||
```bash
|
```bash
|
||||||
docker build -t ollama-proxy .
|
docker build -t ollama-proxy .
|
||||||
docker run -p 11434:11434 -e OPENAI_API_KEY="your-openrouter-api-key" ollama-proxy
|
docker run -p 11434:11434 -e OPENAI_API_KEY="your-openrouter-api-key" ollama-proxy
|
||||||
|
|
||||||
|
# To enable tool use filtering
|
||||||
|
docker run -p 11434:11434 -e OPENAI_API_KEY="your-openrouter-api-key" -e TOOL_USE_ONLY=true ollama-proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ollama-proxy:
|
ollama-proxy:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "11434:11434"
|
- "21434:11434"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||||
- FREE_MODE=${FREE_MODE:-true}
|
- FREE_MODE=${FREE_MODE:-true}
|
||||||
|
- TOOL_USE_ONLY=${TOOL_USE_ONLY:-false}
|
||||||
volumes:
|
volumes:
|
||||||
- ./models-filter:/models-filter:ro
|
- ./models-filter:/models-filter:ro
|
||||||
- proxy-data:/data
|
- proxy-data:/data
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type orModels struct {
|
|||||||
Data []struct {
|
Data []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
ContextLength int `json:"context_length"`
|
ContextLength int `json:"context_length"`
|
||||||
|
SupportedParameters []string `json:"supported_parameters"`
|
||||||
TopProvider struct {
|
TopProvider struct {
|
||||||
ContextLength int `json:"context_length"`
|
ContextLength int `json:"context_length"`
|
||||||
} `json:"top_provider"`
|
} `json:"top_provider"`
|
||||||
@@ -24,6 +25,16 @@ type orModels struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// supportsToolUse checks if a model supports tool use by looking for "tools" in supported_parameters
|
||||||
|
func supportsToolUse(supportedParams []string) bool {
|
||||||
|
for _, param := range supportedParams {
|
||||||
|
if param == "tools" || param == "tool_choice" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func fetchFreeModels(apiKey string) ([]string, error) {
|
func fetchFreeModels(apiKey string) ([]string, error) {
|
||||||
req, err := http.NewRequest("GET", "https://openrouter.ai/api/v1/models", nil)
|
req, err := http.NewRequest("GET", "https://openrouter.ai/api/v1/models", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -42,6 +53,10 @@ func fetchFreeModels(apiKey string) ([]string, error) {
|
|||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if tool use filtering is enabled
|
||||||
|
toolUseOnly := strings.ToLower(os.Getenv("TOOL_USE_ONLY")) == "true"
|
||||||
|
|
||||||
type item struct {
|
type item struct {
|
||||||
id string
|
id string
|
||||||
ctx int
|
ctx int
|
||||||
@@ -49,6 +64,11 @@ func fetchFreeModels(apiKey string) ([]string, error) {
|
|||||||
var items []item
|
var items []item
|
||||||
for _, m := range result.Data {
|
for _, m := range result.Data {
|
||||||
if m.Pricing.Prompt == "0" && m.Pricing.Completion == "0" {
|
if m.Pricing.Prompt == "0" && m.Pricing.Completion == "0" {
|
||||||
|
// If tool use filtering is enabled, skip models that don't support tools
|
||||||
|
if toolUseOnly && !supportsToolUse(m.SupportedParameters) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
ctx := m.TopProvider.ContextLength
|
ctx := m.TopProvider.ContextLength
|
||||||
if ctx == 0 {
|
if ctx == 0 {
|
||||||
ctx = m.ContextLength
|
ctx = m.ContextLength
|
||||||
|
|||||||
129
main.go
129
main.go
@@ -101,6 +101,9 @@ func main() {
|
|||||||
r.GET("/api/tags", func(c *gin.Context) {
|
r.GET("/api/tags", func(c *gin.Context) {
|
||||||
var newModels []map[string]interface{}
|
var newModels []map[string]interface{}
|
||||||
|
|
||||||
|
// Check if tool use filtering is enabled
|
||||||
|
toolUseOnly := strings.ToLower(os.Getenv("TOOL_USE_ONLY")) == "true"
|
||||||
|
|
||||||
if freeMode {
|
if freeMode {
|
||||||
// In free mode, show only available free models
|
// In free mode, show only available free models
|
||||||
currentTime := time.Now().Format(time.RFC3339)
|
currentTime := time.Now().Format(time.RFC3339)
|
||||||
@@ -142,6 +145,72 @@ func main() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-free mode: use original logic
|
// Non-free mode: use original logic
|
||||||
|
if toolUseOnly {
|
||||||
|
// If tool use filtering is enabled, we need to fetch full model details from OpenRouter
|
||||||
|
req, err := http.NewRequest("GET", "https://openrouter.ai/api/v1/models", nil)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error creating request for models", "Error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error fetching models from OpenRouter", "Error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
slog.Error("Unexpected status from OpenRouter", "status", resp.Status)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch models"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var result orModels
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
slog.Error("Error decoding models response", "Error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter models based on tool use support and model filter
|
||||||
|
currentTime := time.Now().Format(time.RFC3339)
|
||||||
|
newModels = make([]map[string]interface{}, 0, len(result.Data))
|
||||||
|
for _, m := range result.Data {
|
||||||
|
if !supportsToolUse(m.SupportedParameters) {
|
||||||
|
continue // Skip models that don't support tool use
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract display name from full model name
|
||||||
|
parts := strings.Split(m.ID, "/")
|
||||||
|
displayName := parts[len(parts)-1]
|
||||||
|
|
||||||
|
// Apply model filter if it exists
|
||||||
|
if !isModelInFilter(displayName, modelFilter) {
|
||||||
|
continue // Skip models not in filter
|
||||||
|
}
|
||||||
|
|
||||||
|
newModels = append(newModels, map[string]interface{}{
|
||||||
|
"name": displayName,
|
||||||
|
"model": displayName,
|
||||||
|
"modified_at": currentTime,
|
||||||
|
"size": 270898672,
|
||||||
|
"digest": "9077fe9d2ae1a4a41a868836b56b8163731a8fe16621397028c2c76f838c6907",
|
||||||
|
"details": map[string]interface{}{
|
||||||
|
"parent_model": "",
|
||||||
|
"format": "gguf",
|
||||||
|
"family": "tool-enabled",
|
||||||
|
"families": []string{"tool-enabled"},
|
||||||
|
"parameter_size": "varies",
|
||||||
|
"quantization_level": "Q4_K_M",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standard non-free mode: get all models from provider
|
||||||
models, err := provider.GetModels()
|
models, err := provider.GetModels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Error getting models", "Error", err)
|
slog.Error("Error getting models", "Error", err)
|
||||||
@@ -167,6 +236,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"models": newModels})
|
c.JSON(http.StatusOK, gin.H{"models": newModels})
|
||||||
})
|
})
|
||||||
@@ -565,6 +635,9 @@ func main() {
|
|||||||
r.GET("/v1/models", func(c *gin.Context) {
|
r.GET("/v1/models", func(c *gin.Context) {
|
||||||
var models []gin.H
|
var models []gin.H
|
||||||
|
|
||||||
|
// Check if tool use filtering is enabled
|
||||||
|
toolUseOnly := strings.ToLower(os.Getenv("TOOL_USE_ONLY")) == "true"
|
||||||
|
|
||||||
if freeMode {
|
if freeMode {
|
||||||
// In free mode, show only available free models
|
// In free mode, show only available free models
|
||||||
slog.Info("Free mode enabled for /v1/models", "totalFreeModels", len(freeModels), "filterSize", len(modelFilter))
|
slog.Info("Free mode enabled for /v1/models", "totalFreeModels", len(freeModels), "filterSize", len(modelFilter))
|
||||||
@@ -603,6 +676,61 @@ func main() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-free mode: get all models from provider
|
// Non-free mode: get all models from provider
|
||||||
|
if toolUseOnly {
|
||||||
|
// If tool use filtering is enabled, we need to fetch full model details from OpenRouter
|
||||||
|
req, err := http.NewRequest("GET", "https://openrouter.ai/api/v1/models", nil)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error creating request for models", "Error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"message": err.Error()}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error fetching models from OpenRouter", "Error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"message": err.Error()}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
slog.Error("Unexpected status from OpenRouter", "status", resp.Status)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"message": "Failed to fetch models"}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var result orModels
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
slog.Error("Error decoding models response", "Error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"message": err.Error()}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter models based on tool use support and model filter
|
||||||
|
for _, m := range result.Data {
|
||||||
|
if !supportsToolUse(m.SupportedParameters) {
|
||||||
|
continue // Skip models that don't support tool use
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract display name from full model name
|
||||||
|
parts := strings.Split(m.ID, "/")
|
||||||
|
displayName := parts[len(parts)-1]
|
||||||
|
|
||||||
|
// Apply model filter if it exists
|
||||||
|
if !isModelInFilter(displayName, modelFilter) {
|
||||||
|
continue // Skip models not in filter
|
||||||
|
}
|
||||||
|
|
||||||
|
models = append(models, gin.H{
|
||||||
|
"id": displayName,
|
||||||
|
"object": "model",
|
||||||
|
"created": time.Now().Unix(),
|
||||||
|
"owned_by": "openrouter",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standard non-free mode: get all models from provider
|
||||||
providerModels, err := provider.GetModels()
|
providerModels, err := provider.GetModels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Error getting models", "Error", err)
|
slog.Error("Error getting models", "Error", err)
|
||||||
@@ -624,6 +752,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
slog.Info("Returning models response", "modelCount", len(models))
|
slog.Info("Returning models response", "modelCount", len(models))
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
Reference in New Issue
Block a user