diff --git a/main.go b/main.go index d58bf1a..11f8261 100644 --- a/main.go +++ b/main.go @@ -4,8 +4,8 @@ import ( "bytes" "encoding/json" "errors" - "fmt" "flag" + "fmt" "io" "log/slog" "net/http" @@ -210,10 +210,10 @@ func runBench(questions []models.Question) ([]models.Answer, error) { logger.Error("failed to parse llm response", "error", err) continue } - + // Process the response to detect tool usage respText, toolCall := processLLMResponse(respText) - + a := models.Answer{ Q: q, Answer: respText, @@ -243,7 +243,7 @@ func callLLM(prompt string, apiURL string) ([]byte, error) { } } - maxRetries := 6 + maxRetries := 3 baseDelay := 2 * time.Second for attempt := 0; attempt < maxRetries; attempt++ { diff --git a/models/models.go b/models/models.go index 2a1d782..999d16b 100644 --- a/models/models.go +++ b/models/models.go @@ -74,6 +74,49 @@ type DSResp struct { Object string `json:"object"` } +// OpenRouter chat completions response (supports tool calls) +type ORChatRespChoice struct { + Index int `json:"index"` + Message struct { + Role string `json:"role"` + Content string `json:"content"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + } `json:"message"` + FinishReason string `json:"finish_reason"` +} + +type ORChatResp struct { + ID string `json:"id"` + Choices []ORChatRespChoice `json:"choices"` + Created int `json:"created"` + Model string `json:"model"` + Object string `json:"object"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} + +// OpenRouter completions response (text only) +type ORCompletionResp struct { + ID string `json:"id"` + Object string `json:"object"` + Created int `json:"created"` + Model string `json:"model"` + Choices []struct { + Text string `json:"text"` + Index int `json:"index"` + Logprobs any `json:"logprobs"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} + type LLMResp struct { Index int `json:"index"` Content string `json:"content"` diff --git a/or.go b/or.go new file mode 100644 index 0000000..db9f821 --- /dev/null +++ b/or.go @@ -0,0 +1,86 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" +) + +var ( + ormodelsLink = "https://openrouter.ai/api/v1/models" + ORFreeModels = []string{ + "google/gemini-2.0-flash-exp:free", + "deepseek/deepseek-chat-v3-0324:free", + "mistralai/mistral-small-3.2-24b-instruct:free", + "qwen/qwen3-14b:free", + "google/gemma-3-27b-it:free", + "meta-llama/llama-3.3-70b-instruct:free", + } +) + +type ORModel struct { + ID string `json:"id"` + CanonicalSlug string `json:"canonical_slug"` + HuggingFaceID string `json:"hugging_face_id"` + Name string `json:"name"` + Created int `json:"created"` + Description string `json:"description"` + ContextLength int `json:"context_length"` + Architecture struct { + Modality string `json:"modality"` + InputModalities []string `json:"input_modalities"` + OutputModalities []string `json:"output_modalities"` + Tokenizer string `json:"tokenizer"` + InstructType any `json:"instruct_type"` + } `json:"architecture"` + Pricing struct { + Prompt string `json:"prompt"` + Completion string `json:"completion"` + Request string `json:"request"` + Image string `json:"image"` + Audio string `json:"audio"` + WebSearch string `json:"web_search"` + InternalReasoning string `json:"internal_reasoning"` + } `json:"pricing,omitempty"` + TopProvider struct { + ContextLength int `json:"context_length"` + MaxCompletionTokens int `json:"max_completion_tokens"` + IsModerated bool `json:"is_moderated"` + } `json:"top_provider"` + PerRequestLimits any `json:"per_request_limits"` + SupportedParameters []string `json:"supported_parameters"` +} + +// https://openrouter.ai/api/v1/models +type ORModels struct { + Data []ORModel `json:"data"` +} + +func (orm *ORModels) ListFree() []string { + resp := []string{} + for _, model := range orm.Data { + if model.Pricing.Prompt == "0" && model.Pricing.Request == "0" && + model.Pricing.Completion == "0" { + resp = append(resp, model.ID) + } + } + return resp +} + +func ListORModels() ([]string, error) { + resp, err := http.Get(ormodelsLink) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + err := fmt.Errorf("failed to fetch or models; status: %s", resp.Status) + return nil, err + } + data := &ORModels{} + if err := json.NewDecoder(resp.Body).Decode(data); err != nil { + return nil, err + } + freeModels := data.ListFree() + return freeModels, nil +} diff --git a/parser.go b/parser.go index d4df06e..81a5c0e 100644 --- a/parser.go +++ b/parser.go @@ -152,68 +152,99 @@ func (p *lcpRespParser) MakePayload(prompt string) io.Reader { type openRouterParser struct { log *slog.Logger modelIndex uint32 + useChatAPI bool + supportsTools bool } func NewOpenRouterParser(log *slog.Logger) *openRouterParser { return &openRouterParser{ log: log, modelIndex: 0, + useChatAPI: false, // Default to completion API which is more widely supported + supportsTools: false, // Don't assume tool support } } func (p *openRouterParser) ParseBytes(body []byte) (string, error) { - // parsing logic here - resp := models.DSResp{} - if err := json.Unmarshal(body, &resp); err != nil { - p.log.Error("failed to unmarshal", "error", err) - return "", err - } - if len(resp.Choices) == 0 { - p.log.Error("empty choices", "resp", resp) - err := errors.New("empty choices in resp") - return "", err - } - - // Check if the response contains tool calls - choice := resp.Choices[0] - - // Handle response with message field (OpenAI format) - if choice.Message.Role != "" { + // If using chat API, parse as chat completion response (supports tool calls) + if p.useChatAPI { + resp := models.ORChatResp{} + if err := json.Unmarshal(body, &resp); err != nil { + p.log.Error("failed to unmarshal openrouter chat response", "error", err) + return "", err + } + if len(resp.Choices) == 0 { + p.log.Error("empty choices in openrouter chat response", "resp", resp) + err := errors.New("empty choices in openrouter chat response") + return "", err + } + + choice := resp.Choices[0] + + // Check if the response contains tool calls if len(choice.Message.ToolCalls) > 0 { // Handle tool call response toolCall := choice.Message.ToolCalls[0] // Return a special marker indicating tool usage return fmt.Sprintf("[TOOL_CALL:%s]", toolCall.Function.Name), nil } + // Regular text response return choice.Message.Content, nil } - // Handle response with text field (legacy format) - return choice.Text, nil + // If using completion API, parse as text completion response (no tool calls) + resp := models.ORCompletionResp{} + if err := json.Unmarshal(body, &resp); err != nil { + p.log.Error("failed to unmarshal openrouter completion response", "error", err) + return "", err + } + if len(resp.Choices) == 0 { + p.log.Error("empty choices in openrouter completion response", "resp", resp) + err := errors.New("empty choices in openrouter completion response") + return "", err + } + + // Return the text content + return resp.Choices[0].Text, nil } func (p *openRouterParser) MakePayload(prompt string) io.Reader { - // Models to rotate through - // TODO: to config - model := "deepseek/deepseek-r1:free" - // Get next model index using atomic addition for thread safety - p.modelIndex++ + if p.useChatAPI { + // Use chat completions API with messages format (supports tool calls) + payload := struct { + Model string `json:"model"` + Messages []models.RoleMsg `json:"messages"` + }{ + Model: "openai/gpt-4o-mini", + Messages: []models.RoleMsg{ + {Role: "user", Content: prompt}, + }, + } + + b, err := json.Marshal(payload) + if err != nil { + p.log.Error("failed to marshal openrouter chat payload", "error", err) + return nil + } + p.log.Debug("made openrouter chat payload", "payload", string(b)) + return bytes.NewReader(b) + } + + // Use completions API with prompt format (no tool calls) payload := struct { - Model string `json:"model"` - Prompt string `json:"prompt"` - Tools []models.Tool `json:"tools,omitempty"` + Model string `json:"model"` + Prompt string `json:"prompt"` }{ - Model: model, + Model: "openai/gpt-4o-mini", Prompt: prompt, - Tools: baseTools, // Include the tools in the request } b, err := json.Marshal(payload) if err != nil { - p.log.Error("failed to marshal openrouter payload", "error", err) + p.log.Error("failed to marshal openrouter completion payload", "error", err) return nil } - p.log.Debug("made openrouter payload", "model", model, "payload", string(b)) + p.log.Debug("made openrouter completion payload", "payload", string(b)) return bytes.NewReader(b) }