Files

205 lines
5.1 KiB
Go

// Package modelquery provides a unified interface to query available models
// from different LLM providers (Ollama, OpenRouter, Gemini).
package modelquery
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// ProviderType represents the type of LLM provider.
type ProviderType string
const (
ProviderOllama ProviderType = "ollama"
ProviderOpenRouter ProviderType = "openrouter"
ProviderGemini ProviderType = "gemini"
ProviderHuggingFace ProviderType = "huggingface"
)
// Model represents a machine learning model with its metadata.
type Model struct {
ID string // Unique identifier (e.g., "llama3", "google/gemini-2.0-flash")
Name string // Human-readable name
Description string // Description of the model
ContextLen int // Context window size in tokens (0 if unknown)
}
// Provider represents configuration for connecting to an LLM provider.
type Provider struct {
Type ProviderType // Provider type: ollama, openrouter, gemini
Endpoint string // Base URL for the API
APIKey string // API key (optional for some providers)
}
// ListModels queries the provider for a list of available models.
// It returns a slice of Model or an error if the request fails.
//
// Example usage:
//
// models, err := modelquery.ListModels(ctx, provider)
// if err != nil {
// log.Fatal(err)
// }
// for _, m := range models {
// fmt.Println(m.ID, m.Name)
// }
func ListModels(ctx context.Context, p Provider) ([]Model, error) {
return ListModelsWithQuery(ctx, p, "")
}
func ListModelsWithQuery(ctx context.Context, p Provider, query string) ([]Model, error) {
switch p.Type {
case ProviderOllama:
return listOllamaModels(ctx, p)
case ProviderOpenRouter:
return listOpenRouterModels(ctx, p, query)
case ProviderGemini:
return listGeminiModels(ctx, p)
case ProviderHuggingFace:
return listHuggingFaceModels(ctx, p, query)
default:
return nil, fmt.Errorf("unsupported provider type: %s", p.Type)
}
}
func SearchModels(ctx context.Context, p Provider, query string) ([]Model, error) {
return SearchModelsWithQuery(ctx, p, query)
}
func SearchModelsWithQuery(ctx context.Context, p Provider, query string) ([]Model, error) {
query = normalizeQuery(query)
if query == "" {
return ListModelsWithQuery(ctx, p, "")
}
switch p.Type {
case ProviderOllama:
models, err := listOllamaModels(ctx, p)
if err != nil {
return nil, err
}
return filterModels(models, query), nil
case ProviderOpenRouter:
models, err := listOpenRouterModels(ctx, p, "")
if err != nil {
return nil, err
}
return filterModels(models, query), nil
case ProviderGemini:
models, err := listGeminiModels(ctx, p)
if err != nil {
return nil, err
}
return filterModels(models, query), nil
case ProviderHuggingFace:
return listHuggingFaceModels(ctx, p, query)
default:
return nil, fmt.Errorf("unsupported provider type: %s", p.Type)
}
}
func filterModels(models []Model, query string) []Model {
var result []Model
for _, m := range models {
if contains(m.ID, query) || contains(m.Name, query) || contains(m.Description, query) {
result = append(result, m)
}
}
return result
}
// normalizeQuery lowercases and trims the query string.
func normalizeQuery(q string) string {
var result []byte
for i := 0; i < len(q); i++ {
c := q[i]
if c >= 'A' && c <= 'Z' {
c += 'a' - 'A'
}
if c != ' ' && c != '\t' && c != '\n' && c != '\r' {
result = append(result, c)
}
}
return string(result)
}
// contains checks if s contains substr (case-insensitive).
func contains(s, substr string) bool {
if len(substr) == 0 {
return true
}
if len(substr) > len(s) {
return false
}
for i := 0; i <= len(s)-len(substr); i++ {
if equalFold(s[i:i+len(substr)], substr) {
return true
}
}
return false
}
// equalFold is a case-insensitive string comparison.
func equalFold(a, b string) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
ca, cb := a[i], b[i]
if ca >= 'A' && ca <= 'Z' {
ca += 'a' - 'A'
}
if cb >= 'A' && cb <= 'Z' {
cb += 'a' - 'A'
}
if ca != cb {
return false
}
}
return true
}
// doRequest performs an HTTP request and decodes the response.
func doRequest(ctx context.Context, method, url string, apiKey string, body io.Reader) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if apiKey != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
respBody, _ := io.ReadAll(resp.Body)
return respBody, nil
}
// addParams adds query parameters to a URL.
func addParams(baseURL string, params map[string]string) string {
if len(params) == 0 {
return baseURL
}
values := url.Values{}
for k, v := range params {
values.Set(k, v)
}
return baseURL + "?" + values.Encode()
}