package providers import ( "context" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" ) // OpenRouterProvider handles the OpenRouter unified API. // OpenRouter is OpenAI-compatible with 300+ models, per-model pricing, // and architecture metadata. Requires HTTP-Referer and X-Title headers. // // Ported from ai-editor js/providers/openrouter.js type OpenRouterProvider struct{} func (p *OpenRouterProvider) ID() string { return "openrouter" } // ── Chat Completion ───────────────────────── // Delegates to OpenAI provider — OpenRouter is fully OAI-compatible. func (p *OpenRouterProvider) ChatCompletion(ctx context.Context, cfg ProviderConfig, req CompletionRequest) (*CompletionResponse, error) { oai := &OpenAIProvider{} return oai.ChatCompletion(ctx, cfg, req) } func (p *OpenRouterProvider) StreamCompletion(ctx context.Context, cfg ProviderConfig, req CompletionRequest) (<-chan StreamEvent, error) { oai := &OpenAIProvider{} return oai.StreamCompletion(ctx, cfg, req) } // ── List Models ───────────────────────────── // OpenRouter /v1/models returns architecture, pricing, context_length. func (p *OpenRouterProvider) ListModels(ctx context.Context, cfg ProviderConfig) ([]Model, error) { endpoint := strings.TrimSuffix(cfg.Endpoint, "/") + "/models" httpReq, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) if err != nil { return nil, err } if cfg.APIKey != "" { httpReq.Header.Set("Authorization", "Bearer "+cfg.APIKey) } for k, v := range cfg.CustomHeaders { httpReq.Header.Set(k, v) } resp, err := http.DefaultClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("openrouter list models: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("openrouter list models: HTTP %d: %s", resp.StatusCode, string(b)) } var result orModelsResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("openrouter decode models: %w", err) } models := make([]Model, 0, len(result.Data)) for _, m := range result.Data { // Start with known table, fall back to heuristic caps, found := LookupKnownModel(m.ID) if !found { caps = InferCapabilities(m.ID) } // Overlay context length from OpenRouter metadata if m.ContextLength > 0 && caps.MaxContext == 0 { caps.MaxContext = m.ContextLength } // Overlay max output from OpenRouter if available if m.TopProvider.MaxCompletionTokens > 0 && caps.MaxOutputTokens == 0 { caps.MaxOutputTokens = m.TopProvider.MaxCompletionTokens } // Vision from architecture modality arch := m.Architecture if arch.Modality == "multimodal" { caps.Vision = true } // Streaming is universal on OpenRouter caps.Streaming = true // Parse pricing (OpenRouter uses per-token strings, convert to per-1M) var pricing *ModelPricing if m.Pricing.Prompt != "" { inputPerToken, _ := strconv.ParseFloat(m.Pricing.Prompt, 64) outputPerToken, _ := strconv.ParseFloat(m.Pricing.Completion, 64) if inputPerToken > 0 || outputPerToken > 0 { pricing = &ModelPricing{ InputPerM: inputPerToken * 1_000_000, OutputPerM: outputPerToken * 1_000_000, } } } // Owner from model ID prefix (e.g. "anthropic/claude-3-opus" → "anthropic") ownedBy := "" if idx := strings.Index(m.ID, "/"); idx >= 0 { ownedBy = m.ID[:idx] } name := m.Name if name == "" { name = m.ID } models = append(models, Model{ ID: m.ID, Name: name, OwnedBy: ownedBy, Capabilities: caps, Pricing: pricing, }) } return models, nil } // ── OpenRouter Wire Types ─────────────────── type orModelsResponse struct { Data []orModel `json:"data"` } type orModel struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` ContextLength int `json:"context_length"` Architecture orArch `json:"architecture"` Pricing orPricing `json:"pricing"` TopProvider orTopProvider `json:"top_provider"` } type orArch struct { Modality string `json:"modality"` Tokenizer string `json:"tokenizer"` InstructType string `json:"instruct_type"` } type orPricing struct { Prompt string `json:"prompt"` // per-token cost as string Completion string `json:"completion"` // per-token cost as string } type orTopProvider struct { MaxCompletionTokens int `json:"max_completion_tokens"` IsModerated bool `json:"is_moderated"` }