mirror of
https://github.com/cheikh2shift/godex.git
synced 2026-10-21 10:40:08 -05:00
205 lines
5.1 KiB
Go
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()
|
|
}
|