2025-08-05 03:56:23 +02:00
package main
import (
2025-08-14 03:53:14 +02:00
"context"
2025-08-05 03:56:23 +02:00
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
2025-08-14 03:53:14 +02:00
"strings"
2025-08-05 03:56:23 +02:00
"github.com/revrost/go-openrouter"
)
2025-08-14 03:53:14 +02:00
type ToolCall struct {
ID string ` json:"id" `
Name string ` json:"name" `
Args string ` json:"args" `
Result string ` json:"result,omitempty" `
2025-08-14 17:08:45 +02:00
Done bool ` json:"done,omitempty" `
2025-08-14 03:53:14 +02:00
}
2025-08-05 03:56:23 +02:00
type Message struct {
2025-08-14 03:53:14 +02:00
Role string ` json:"role" `
Text string ` json:"text" `
Tool * ToolCall ` json:"tool" `
2025-08-05 03:56:23 +02:00
}
2025-08-10 22:32:40 +02:00
type Reasoning struct {
Effort string ` json:"effort" `
Tokens int ` json:"tokens" `
}
2025-08-05 03:56:23 +02:00
type Request struct {
2025-08-07 22:09:08 +02:00
Prompt string ` json:"prompt" `
2025-08-05 03:56:23 +02:00
Model string ` json:"model" `
Temperature float64 ` json:"temperature" `
2025-08-11 00:15:58 +02:00
JSON bool ` json:"json" `
Search bool ` json:"search" `
2025-08-10 22:32:40 +02:00
Reasoning Reasoning ` json:"reasoning" `
2025-08-05 03:56:23 +02:00
Messages [ ] Message ` json:"messages" `
}
2025-08-14 03:53:14 +02:00
func ( t * ToolCall ) AsToolCall ( ) openrouter . ToolCall {
return openrouter . ToolCall {
ID : t . ID ,
Type : openrouter . ToolTypeFunction ,
Function : openrouter . FunctionCall {
Name : t . Name ,
Arguments : t . Args ,
} ,
}
}
func ( t * ToolCall ) AsToolMessage ( ) openrouter . ChatCompletionMessage {
return openrouter . ChatCompletionMessage {
Role : openrouter . ChatMessageRoleTool ,
ToolCallID : t . ID ,
Content : openrouter . Content {
Text : t . Result ,
} ,
}
}
2025-08-05 03:56:23 +02:00
func ( r * Request ) Parse ( ) ( * openrouter . ChatCompletionRequest , error ) {
var request openrouter . ChatCompletionRequest
2025-08-07 22:09:08 +02:00
model , ok := ModelMap [ r . Model ]
if ! ok {
2025-08-05 03:56:23 +02:00
return nil , fmt . Errorf ( "unknown model: %q" , r . Model )
}
request . Model = r . Model
2025-08-10 22:39:18 +02:00
if r . Temperature < 0 || r . Temperature > 2 {
return nil , fmt . Errorf ( "invalid temperature (0-2): %f" , r . Temperature )
2025-08-05 03:56:23 +02:00
}
request . Temperature = float32 ( r . Temperature )
2025-08-10 22:32:40 +02:00
if model . Reasoning {
request . Reasoning = & openrouter . ChatCompletionReasoning { }
switch r . Reasoning . Effort {
case "high" , "medium" , "low" :
request . Reasoning . Effort = & r . Reasoning . Effort
default :
if r . Reasoning . Tokens <= 0 || r . Reasoning . Tokens > 1024 * 1024 {
return nil , fmt . Errorf ( "invalid reasoning tokens (1-1048576): %d" , r . Reasoning . Tokens )
}
request . Reasoning . MaxTokens = & r . Reasoning . Tokens
}
}
2025-08-11 00:15:58 +02:00
if model . JSON && r . JSON {
request . ResponseFormat = & openrouter . ChatCompletionResponseFormat {
Type : openrouter . ChatCompletionResponseFormatTypeJSONObject ,
}
}
2025-08-14 17:08:45 +02:00
if model . Tools && r . Search && ExaToken != "" {
request . Tools = GetSearchTools ( )
2025-08-14 03:53:14 +02:00
request . ToolChoice = "auto"
2025-08-14 17:08:45 +02:00
request . Messages = append ( request . Messages , openrouter . SystemMessage ( "You have access to web search tools. Use `search_web` with `query` (string) and `num_results` (1-10) to find current information and get result summaries. Use `fetch_contents` with `urls` (array) to read full page content. Always specify all parameters for each tool call. Call only one tool per response." ) )
2025-08-11 00:15:58 +02:00
}
2025-08-07 22:09:08 +02:00
prompt , err := BuildPrompt ( r . Prompt , model )
if err != nil {
return nil , err
}
if prompt != "" {
request . Messages = append ( request . Messages , openrouter . SystemMessage ( prompt ) )
}
2025-08-05 03:56:23 +02:00
for index , message := range r . Messages {
2025-08-14 03:53:14 +02:00
switch message . Role {
case "system" , "user" :
request . Messages = append ( request . Messages , openrouter . ChatCompletionMessage {
Role : message . Role ,
Content : openrouter . Content {
Text : message . Text ,
} ,
} )
case "assistant" :
msg := openrouter . ChatCompletionMessage {
Role : openrouter . ChatMessageRoleAssistant ,
Content : openrouter . Content {
Text : message . Text ,
} ,
}
tool := message . Tool
if tool != nil {
msg . ToolCalls = [ ] openrouter . ToolCall { tool . AsToolCall ( ) }
request . Messages = append ( request . Messages , msg )
msg = tool . AsToolMessage ( )
}
request . Messages = append ( request . Messages , msg )
default :
2025-08-05 03:56:23 +02:00
return nil , fmt . Errorf ( "[%d] invalid role: %q" , index + 1 , message . Role )
}
}
return & request , nil
}
func HandleChat ( w http . ResponseWriter , r * http . Request ) {
2025-08-14 17:08:45 +02:00
debug ( "new chat" )
2025-08-05 03:56:23 +02:00
var raw Request
if err := json . NewDecoder ( r . Body ) . Decode ( & raw ) ; err != nil {
RespondJson ( w , http . StatusBadRequest , map [ string ] any {
"error" : err . Error ( ) ,
} )
return
}
request , err := raw . Parse ( )
if err != nil {
RespondJson ( w , http . StatusBadRequest , map [ string ] any {
"error" : err . Error ( ) ,
} )
return
}
request . Stream = true
2025-08-14 03:53:14 +02:00
response , err := NewStream ( w )
2025-08-05 03:56:23 +02:00
if err != nil {
RespondJson ( w , http . StatusBadRequest , map [ string ] any {
2025-08-14 03:53:14 +02:00
"error" : err . Error ( ) ,
2025-08-05 03:56:23 +02:00
} )
return
}
2025-08-14 17:08:45 +02:00
debug ( "handling request" )
2025-08-14 03:53:14 +02:00
ctx := r . Context ( )
2025-08-05 03:56:23 +02:00
2025-08-14 03:53:14 +02:00
for iteration := range MaxIterations {
2025-08-14 17:08:45 +02:00
debug ( "iteration %d of %d" , iteration + 1 , MaxIterations )
2025-08-14 03:53:14 +02:00
if iteration == MaxIterations - 1 {
2025-08-14 17:08:45 +02:00
debug ( "no more tool calls" )
2025-08-14 03:53:14 +02:00
request . Tools = nil
request . ToolChoice = ""
2025-08-14 17:08:45 +02:00
request . Messages = append ( request . Messages , openrouter . SystemMessage ( "You have reached the maximum number of tool calls for this conversation. Provide your final response based on the information you have gathered." ) )
2025-08-14 03:53:14 +02:00
}
2025-08-05 03:56:23 +02:00
2025-08-14 03:53:14 +02:00
tool , message , err := RunCompletion ( ctx , response , request )
if err != nil {
response . Send ( ErrorChunk ( err ) )
return
}
2025-08-14 17:08:45 +02:00
if tool == nil {
debug ( "no tool call, done" )
2025-08-14 03:53:14 +02:00
return
}
2025-08-14 17:08:45 +02:00
debug ( "got %q tool call" , tool . Name )
2025-08-14 03:53:14 +02:00
response . Send ( ToolChunk ( tool ) )
2025-08-14 17:08:45 +02:00
switch tool . Name {
case "search_web" :
err = HandleSearchWebTool ( ctx , tool )
if err != nil {
response . Send ( ErrorChunk ( err ) )
2025-08-14 03:53:14 +02:00
2025-08-14 17:08:45 +02:00
return
}
case "fetch_contents" :
err = HandleFetchContentsTool ( ctx , tool )
if err != nil {
response . Send ( ErrorChunk ( err ) )
return
}
default :
2025-08-14 03:53:14 +02:00
return
}
2025-08-14 17:08:45 +02:00
tool . Done = true
debug ( "finished tool call" )
2025-08-14 03:53:14 +02:00
response . Send ( ToolChunk ( tool ) )
request . Messages = append ( request . Messages ,
openrouter . ChatCompletionMessage {
Role : openrouter . ChatMessageRoleAssistant ,
Content : openrouter . Content {
Text : message ,
} ,
ToolCalls : [ ] openrouter . ToolCall { tool . AsToolCall ( ) } ,
} ,
tool . AsToolMessage ( ) ,
)
2025-08-05 03:56:23 +02:00
}
2025-08-14 03:53:14 +02:00
}
2025-08-05 03:56:23 +02:00
2025-08-14 03:53:14 +02:00
func RunCompletion ( ctx context . Context , response * Stream , request * openrouter . ChatCompletionRequest ) ( * ToolCall , string , error ) {
stream , err := OpenRouterStartStream ( ctx , * request )
if err != nil {
return nil , "" , err
}
defer stream . Close ( )
var (
id string
result strings . Builder
tool * ToolCall
)
2025-08-11 15:43:00 +02:00
2025-08-05 03:56:23 +02:00
for {
chunk , err := stream . Recv ( )
if err != nil {
if errors . Is ( err , io . EOF ) {
2025-08-14 03:53:14 +02:00
break
2025-08-05 03:56:23 +02:00
}
2025-08-14 03:53:14 +02:00
return nil , "" , err
2025-08-05 03:56:23 +02:00
}
2025-08-11 15:43:00 +02:00
if id == "" {
id = chunk . ID
response . Send ( IDChunk ( id ) )
}
2025-08-05 03:56:23 +02:00
if len ( chunk . Choices ) == 0 {
continue
}
choice := chunk . Choices [ 0 ]
if choice . FinishReason == openrouter . FinishReasonContentFilter {
response . Send ( ErrorChunk ( errors . New ( "stopped due to content_filter" ) ) )
2025-08-14 03:53:14 +02:00
return nil , "" , nil
}
calls := choice . Delta . ToolCalls
if len ( calls ) > 0 {
call := calls [ 0 ]
if tool == nil {
tool = & ToolCall { }
}
tool . ID += call . ID
tool . Name += call . Function . Name
tool . Args += call . Function . Arguments
} else if tool != nil {
break
2025-08-05 03:56:23 +02:00
}
content := choice . Delta . Content
if content != "" {
2025-08-14 03:53:14 +02:00
result . WriteString ( content )
2025-08-05 03:56:23 +02:00
response . Send ( TextChunk ( content ) )
} else if choice . Delta . Reasoning != nil {
response . Send ( ReasoningChunk ( * choice . Delta . Reasoning ) )
}
}
2025-08-14 03:53:14 +02:00
return tool , result . String ( ) , nil
2025-08-05 03:56:23 +02:00
}