├── .gitignore ├── go.mod ├── .golangci.yaml ├── go.sum ├── internal ├── strutil │ ├── create_endpoint.go │ ├── remove_leading_slash.go │ ├── remove_leading_slash_test.go │ ├── remove_trailing_slash.go │ ├── create_endpoint_test.go │ └── remove_trailing_slash_test.go ├── debug │ └── print_as_json.go ├── assert │ └── assert.go └── optional │ ├── types.go │ └── types_test.go ├── Taskfile.yml ├── errors.go ├── .github └── workflows │ └── lint-and-test.yaml ├── LICENSE ├── .devcontainer ├── startup.sh ├── devcontainer.json └── Dockerfile ├── examples ├── 01-basic │ └── main.go ├── 02-clone-completion │ └── main.go ├── 06-other-options │ └── main.go ├── 03-reuse-completion │ └── main.go ├── 04-model-fallback │ └── main.go ├── 05-function-calling │ └── main.go └── 07-force-response-format │ └── main.go ├── chat_completion_message.go ├── chat_completion_response.go ├── client.go ├── README.md └── chat_completion.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary files and directories 2 | *.tmp 3 | temp/ 4 | tmp/ 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eduardolat/openroutergo 2 | 3 | go 1.22 4 | 5 | require github.com/orsinium-labs/enum v1.4.0 6 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: "5m" 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - errcheck 8 | - gosimple 9 | - govet 10 | - ineffassign 11 | - staticcheck 12 | - unused 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= 2 | github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 3 | github.com/orsinium-labs/enum v1.4.0 h1:3NInlfV76kuAg0kq2FFUondmg3WO7gMEgrPPrlzLDUM= 4 | github.com/orsinium-labs/enum v1.4.0/go.mod h1:Qj5IK2pnElZtkZbGDxZMjpt7SUsn4tqE5vRelmWaBbc= 5 | -------------------------------------------------------------------------------- /internal/strutil/create_endpoint.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | // CreateEndpoint creates an endpoint from a base URL and a path. 4 | // 5 | // It removes all the trailing slashes from the base URL and all the leading slashes 6 | // from the path, then joins them together with a single slash in between. 7 | func CreateEndpoint(baseURL, path string) string { 8 | return RemoveTrailingSlashes(baseURL) + "/" + RemoveLeadingSlashes(path) 9 | } 10 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: "3" 4 | 5 | tasks: 6 | test: 7 | desc: Run all tests on this repo 8 | cmd: go test ./... 9 | 10 | lint: 11 | desc: Lint all code on this repo 12 | cmd: golangci-lint run ./... 13 | 14 | fmt: 15 | desc: Format all code on this repo 16 | cmds: 17 | - go fmt ./... 18 | - deno fmt 19 | 20 | fixperms: 21 | desc: Fix permissions for all files and directories 22 | cmd: chmod -R 777 . 23 | -------------------------------------------------------------------------------- /internal/debug/print_as_json.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // PrintAsJSON prints a value as a formatted JSON string. 9 | // 10 | // WARNING: This function is intended for debugging purposes only. Do not use it in production. 11 | func PrintAsJSON(v any) { 12 | b, err := json.MarshalIndent(v, "", " ") 13 | if err != nil { 14 | fmt.Printf("debug print as JSON failed to marshal: %v\n", err) 15 | return 16 | } 17 | 18 | fmt.Println(string(b)) 19 | } 20 | -------------------------------------------------------------------------------- /internal/strutil/remove_leading_slash.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import "strings" 4 | 5 | // RemoveLeadingSlash removes a leading slash from a string. 6 | // 7 | // If the string does not start with a slash, it is returned unchanged. 8 | func RemoveLeadingSlash(str string) string { 9 | return strings.TrimPrefix(str, "/") 10 | } 11 | 12 | // RemoveLeadingSlashes removes leading slashes from a string. 13 | // 14 | // If the string does not start with a slash, it is returned unchanged. 15 | func RemoveLeadingSlashes(str string) string { 16 | for { 17 | if strings.HasPrefix(str, "/") { 18 | str = strings.TrimPrefix(str, "/") 19 | } else { 20 | break 21 | } 22 | } 23 | 24 | return str 25 | } 26 | -------------------------------------------------------------------------------- /internal/strutil/remove_leading_slash_test.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/eduardolat/openroutergo/internal/assert" 7 | ) 8 | 9 | func TestRemoveLeadingSlash(t *testing.T) { 10 | assert.Equal(t, "test", RemoveLeadingSlash("test")) 11 | assert.Equal(t, "test", RemoveLeadingSlash("/test")) 12 | assert.Equal(t, "/test", RemoveLeadingSlash("//test")) 13 | } 14 | 15 | func TestRemoveLeadingSlashes(t *testing.T) { 16 | assert.Equal(t, "test", RemoveLeadingSlashes("test")) 17 | assert.Equal(t, "test", RemoveLeadingSlashes("/test")) 18 | assert.Equal(t, "test", RemoveLeadingSlashes("//test")) 19 | assert.Equal(t, "test", RemoveLeadingSlashes("////////test")) 20 | } 21 | -------------------------------------------------------------------------------- /internal/strutil/remove_trailing_slash.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import "strings" 4 | 5 | // RemoveTrailingSlash removes a trailing slash from a string. 6 | // 7 | // If the string does not end with a slash, it is returned unchanged. 8 | func RemoveTrailingSlash(str string) string { 9 | return strings.TrimSuffix(str, "/") 10 | } 11 | 12 | // RemoveTrailingSlashes removes all trailing slashes from a string. 13 | // 14 | // If the string does not end with a slash, it is returned unchanged. 15 | func RemoveTrailingSlashes(str string) string { 16 | for { 17 | if strings.HasSuffix(str, "/") { 18 | str = strings.TrimSuffix(str, "/") 19 | } else { 20 | break 21 | } 22 | } 23 | 24 | return str 25 | } 26 | -------------------------------------------------------------------------------- /internal/strutil/create_endpoint_test.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/eduardolat/openroutergo/internal/assert" 7 | ) 8 | 9 | func TestCreateEndpoint(t *testing.T) { 10 | assert.Equal(t, "https://example.com/test", CreateEndpoint("https://example.com/", "/test")) 11 | assert.Equal(t, "https://example.com/test", CreateEndpoint("https://example.com/", "test")) 12 | assert.Equal(t, "https://example.com/test/", CreateEndpoint("https://example.com/", "test/")) 13 | assert.Equal(t, "https://example.com/test//", CreateEndpoint("https://example.com/", "test//")) 14 | assert.Equal(t, "https://example.com/api/v1/test", CreateEndpoint("https://example.com/api/v1/", "/test")) 15 | } 16 | -------------------------------------------------------------------------------- /internal/strutil/remove_trailing_slash_test.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/eduardolat/openroutergo/internal/assert" 7 | ) 8 | 9 | func TestRemoveTrailingSlash(t *testing.T) { 10 | assert.Equal(t, "test", RemoveTrailingSlash("test")) 11 | assert.Equal(t, "test", RemoveTrailingSlash("test/")) 12 | assert.Equal(t, "test/", RemoveTrailingSlash("test//")) 13 | } 14 | 15 | func TestRemoveTrailingSlashes(t *testing.T) { 16 | assert.Equal(t, "test", RemoveTrailingSlashes("test")) 17 | assert.Equal(t, "test", RemoveTrailingSlashes("test/")) 18 | assert.Equal(t, "test", RemoveTrailingSlashes("test//")) 19 | assert.Equal(t, "test", RemoveTrailingSlashes("test////////")) 20 | } 21 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package openroutergo 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrBaseURLRequired is returned when the base URL is needed but not provided. 7 | ErrBaseURLRequired = errors.New("the base URL is required") 8 | 9 | // ErrAPIKeyRequired is returned when the API key is needed but not provided. 10 | ErrAPIKeyRequired = errors.New("the API key is required") 11 | 12 | // ErrMessagesRequired is returned when no messages are found and they are needed. 13 | ErrMessagesRequired = errors.New("at least one message is required") 14 | 15 | // ErrAlreadyExecuting is returned when the user tries to execute an action while 16 | // there is already an action in progress. 17 | ErrAlreadyExecuting = errors.New("race condition: the client is currently executing an action") 18 | ) 19 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and test 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - develop 8 | - main 9 | 10 | jobs: 11 | lint-and-test: 12 | name: Lint and test the code 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 20 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Ensure .ssh directory exists even if it's empty 20 | run: mkdir -p /home/runner/.ssh 21 | 22 | - name: Run lint and test 23 | uses: devcontainers/ci@v0.3 24 | with: 25 | push: never 26 | runCmd: > 27 | /bin/bash -c " 28 | go mod download && 29 | task lint && 30 | task test 31 | " 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Luis Eduardo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.devcontainer/startup.sh: -------------------------------------------------------------------------------- 1 | # Define command aliases 2 | alias t='task' 3 | alias td='task dev' 4 | alias tb='task build' 5 | alias tt='task test' 6 | alias tl='task lint' 7 | alias tf='task format' 8 | alias ll='ls -alF' 9 | alias la='ls -A' 10 | alias l='ls -CF' 11 | alias ..='cd ..' 12 | alias c='clear' 13 | echo "[OK] aliases set" 14 | 15 | # Set the user file-creation mode mask to 000, which allows all 16 | # users read, write, and execute permissions for newly created files. 17 | umask 000 18 | echo "[OK] umask set" 19 | 20 | # Run the 'fixperms' task that fixes the permissions of the files and 21 | # directories in the project. 22 | task fixperms 23 | echo "[OK] permissions fixed" 24 | 25 | # Configure Git to ignore ownership and file mode changes. 26 | git config --global --add safe.directory '*' 27 | git config core.fileMode false 28 | echo "[OK] git configured" 29 | 30 | echo " 31 | ──────────────────────────────────────────────────────── 32 | ── Github: https://github.com/eduardolat/openroutergo ── 33 | ──────────────────────────────────────────────────────── 34 | ── Development environment is ready to use! ──────────── 35 | ──────────────────────────────────────────────────────── 36 | " -------------------------------------------------------------------------------- /examples/01-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/eduardolat/openroutergo" 8 | ) 9 | 10 | // This example demonstrates how to use the OpenRouterGo library to create and execute a chat 11 | // completion request. 12 | // 13 | // It provides a step-by-step guide on setting up the client, configuring the chat completion, and 14 | // handling the response. 15 | // 16 | // You can copy this code modify the api key, model, and run it. 17 | 18 | const apiKey = "sk......." 19 | const model = "google/gemini-2.0-flash-exp:free" 20 | 21 | func main() { 22 | client, err := openroutergo. 23 | NewClient(). 24 | WithAPIKey(apiKey). 25 | WithRefererURL("https://my-app.com"). // Optional, for rankings on openrouter.ai 26 | WithRefererTitle("My App"). // Optional, for rankings on openrouter.ai 27 | Create() 28 | if err != nil { 29 | log.Fatalf("Failed to create client: %v", err) 30 | } 31 | 32 | completion := client. 33 | NewChatCompletion(). 34 | WithDebug(true). // Enable debug mode to see the request and response in the console 35 | WithModel(model). // Change the model if you want 36 | WithSystemMessage("You are a helpful assistant expert in geography."). 37 | WithUserMessage("What is the capital of France?") 38 | 39 | _, resp, err := completion.Execute() 40 | if err != nil { 41 | log.Fatalf("Failed to execute completion: %v", err) 42 | } 43 | 44 | fmt.Println("Response:", resp.Choices[0].Message.Content) 45 | } 46 | -------------------------------------------------------------------------------- /examples/02-clone-completion/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/eduardolat/openroutergo" 8 | ) 9 | 10 | // In this example, we create a base chat completion that can be cloned and reused multiple times. 11 | // 12 | // This demonstrates how to set up a reusable chat completion configuration, clone it, and execute 13 | // it with different user messages. 14 | // 15 | // You can copy this code modify the api key, model, and run it. 16 | 17 | const apiKey = "sk......." 18 | const model = "google/gemini-2.0-flash-exp:free" 19 | 20 | func main() { 21 | client, err := openroutergo.NewClient().WithAPIKey(apiKey).Create() 22 | if err != nil { 23 | log.Fatalf("Failed to create client: %v", err) 24 | } 25 | 26 | // Create a base completion to be cloned and reused 27 | baseCompletion := client. 28 | NewChatCompletion(). 29 | WithDebug(true). // Enable debug mode to see the request and response in the console 30 | WithModel(model). // Change the model if you want 31 | WithSystemMessage("You are a helpful assistant expert in geography.") 32 | 33 | // Clone and execute the completion 34 | _, resp, err := baseCompletion. 35 | Clone(). 36 | WithUserMessage("What is the capital of France?"). 37 | Execute() 38 | if err != nil { 39 | log.Fatalf("Failed to execute completion: %v", err) 40 | } 41 | fmt.Println("Response:", resp.Choices[0].Message.Content) 42 | 43 | // Clone the original completion once again and execute it 44 | _, resp, err = baseCompletion. 45 | Clone(). 46 | WithUserMessage("What is the capital of Germany?"). 47 | Execute() 48 | if err != nil { 49 | log.Fatalf("Failed to execute completion: %v", err) 50 | } 51 | fmt.Println("Response:", resp.Choices[0].Message.Content) 52 | } 53 | -------------------------------------------------------------------------------- /examples/06-other-options/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/eduardolat/openroutergo" 10 | ) 11 | 12 | // This example demonstrates how to configure various options for a chat completion request 13 | // using the OpenRouterGo library. 14 | // 15 | // You can copy this code modify the api key, model, and run it. 16 | 17 | const apiKey = "sk......." 18 | const model = "google/gemini-2.0-flash-exp:free" 19 | const modelFallback = "deepseek/deepseek-r1-zero:free" 20 | 21 | func main() { 22 | client, err := openroutergo. 23 | NewClient(). 24 | WithAPIKey(apiKey). 25 | WithTimeout(10 * time.Minute). // Set a timeout for the client requests 26 | WithRefererURL("https://my-app.com"). // Optional, for rankings on openrouter.ai 27 | WithRefererTitle("My App"). // Optional, for rankings on openrouter.ai 28 | Create() 29 | if err != nil { 30 | log.Fatalf("Failed to create client: %v", err) 31 | } 32 | 33 | // You can use your code editor to help you explore all the available options but 34 | // here are some of the most useful ones 35 | completion := client. 36 | NewChatCompletion(). 37 | WithContext(context.Background()). 38 | WithDebug(true). 39 | WithModel(model). 40 | WithModelFallback(modelFallback). 41 | WithSeed(1234567890). 42 | WithTemperature(0.5). 43 | WithMaxPrice(0.5, 2). 44 | WithMaxTokens(1000). 45 | WithSystemMessage("You are a helpful assistant expert in geography."). 46 | WithUserMessage("What is the capital of France?") 47 | 48 | _, resp, err := completion.Execute() 49 | if err != nil { 50 | log.Fatalf("Failed to execute completion: %v", err) 51 | } 52 | 53 | fmt.Println("Response:", resp.Choices[0].Message.Content) 54 | } 55 | -------------------------------------------------------------------------------- /examples/03-reuse-completion/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/eduardolat/openroutergo" 8 | ) 9 | 10 | // In this example, we demonstrate how to start a conversation with the model 11 | // and continue it using the same chat completion. 12 | // 13 | // The example shows how to reuse the chat completion to maintain the conversation 14 | // context, making it easy to continue the dialogue. 15 | // 16 | // You can copy this code modify the api key, model, and run it. 17 | 18 | const apiKey = "sk......." 19 | const model = "google/gemini-2.0-flash-exp:free" 20 | 21 | func main() { 22 | client, err := openroutergo.NewClient().WithAPIKey(apiKey).Create() 23 | if err != nil { 24 | log.Fatalf("Failed to create client: %v", err) 25 | } 26 | 27 | // Create and execute a completion 28 | completion, resp, err := client. 29 | NewChatCompletion(). 30 | WithDebug(true). // Enable debug mode to see the request and response in the console 31 | WithModel(model). // Change the model if you want 32 | WithSystemMessage("You are a helpful assistant expert in geography."). 33 | WithUserMessage("What is the capital of France?"). 34 | Execute() 35 | if err != nil { 36 | log.Fatalf("Failed to execute completion: %v", err) 37 | } 38 | fmt.Println("Response:", resp.Choices[0].Message.Content) 39 | 40 | // Reuse the completion to continue the conversation, the assistant message of the previous 41 | // completion is automatically added so you can continue the conversation easily. 42 | _, resp, err = completion. 43 | WithUserMessage("Thanks! Now, what is the capital of Germany?"). 44 | Execute() 45 | if err != nil { 46 | log.Fatalf("Failed to execute completion: %v", err) 47 | } 48 | fmt.Println("Response:", resp.Choices[0].Message.Content) 49 | } 50 | -------------------------------------------------------------------------------- /internal/assert/assert.go: -------------------------------------------------------------------------------- 1 | // Package assert provides a set of functions for asserting the results of tests. 2 | package assert 3 | 4 | import "testing" 5 | 6 | // Equal asserts that two values are equal. 7 | func Equal[T comparable](t *testing.T, expected T, actual T) { 8 | t.Helper() 9 | if expected != actual { 10 | t.Errorf("expected %v to equal %v", expected, actual) 11 | } 12 | } 13 | 14 | // NotEqual asserts that two values are not equal. 15 | func NotEqual[T comparable](t *testing.T, expected T, actual T) { 16 | t.Helper() 17 | if expected == actual { 18 | t.Errorf("expected %v to not equal %v", expected, actual) 19 | } 20 | } 21 | 22 | // Nil asserts that a value is nil. 23 | func Nil(t *testing.T, actual any) { 24 | t.Helper() 25 | if actual != nil { 26 | t.Errorf("expected %v to be nil", actual) 27 | } 28 | } 29 | 30 | // NotNil asserts that a value is not nil. 31 | func NotNil(t *testing.T, actual any) { 32 | t.Helper() 33 | if actual == nil { 34 | t.Errorf("expected %v to not be nil", actual) 35 | } 36 | } 37 | 38 | // Error asserts that an error is not nil. 39 | func Error(t *testing.T, expected error, actual error) { 40 | t.Helper() 41 | if expected != actual { 42 | t.Errorf("expected %v to equal %v", expected, actual) 43 | } 44 | } 45 | 46 | // NoError asserts that an error is nil. 47 | func NoError(t *testing.T, err error) { 48 | t.Helper() 49 | if err != nil { 50 | t.Errorf("expected no error, got %v", err) 51 | } 52 | } 53 | 54 | // True asserts that a boolean is true. 55 | func True(t *testing.T, actual bool) { 56 | t.Helper() 57 | if !actual { 58 | t.Errorf("expected true, got false") 59 | } 60 | } 61 | 62 | // False asserts that a boolean is false. 63 | func False(t *testing.T, actual bool) { 64 | t.Helper() 65 | if actual { 66 | t.Errorf("expected false, got true") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/04-model-fallback/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/eduardolat/openroutergo" 8 | ) 9 | 10 | // In this example, we set up three fallback models. The idea is to use free models 11 | // first, and only if they fail, a paid model is used as the last fallback. 12 | // 13 | // This demonstrates how to configure fallback models to ensure that the request 14 | // is handled by a model and when it fails because the rate limits, context window, 15 | // or other reasons, the request is automatically retried with a different model. 16 | // 17 | // You can copy this code modify the api key, models, and run it. 18 | 19 | const apiKey = "sk......." 20 | const baseModel = "google/gemini-2.0-flash-exp:free" 21 | const firstFallbackModel = "google/gemini-2.0-flash-thinking-exp-1219:free" 22 | const secondFallbackModel = "deepseek/deepseek-r1-zero:free" 23 | const thirdFallbackModel = "google/gemini-2.0-flash-001" 24 | 25 | func main() { 26 | client, err := openroutergo.NewClient().WithAPIKey(apiKey).Create() 27 | if err != nil { 28 | log.Fatalf("Failed to create client: %v", err) 29 | } 30 | 31 | completion := client. 32 | NewChatCompletion(). 33 | WithDebug(true). // Enable debug mode to see the request and response in the console 34 | WithModel(baseModel). 35 | WithModelFallback(firstFallbackModel). 36 | WithModelFallback(secondFallbackModel). 37 | WithModelFallback(thirdFallbackModel). 38 | WithSystemMessage("You are a helpful assistant expert in geography."). 39 | WithUserMessage("What is the capital of France?") 40 | 41 | _, resp, err := completion.Execute() 42 | if err != nil { 43 | log.Fatalf("Failed to execute completion: %v", err) 44 | } 45 | 46 | fmt.Println("Model used:", resp.Model) 47 | fmt.Println("Provider used:", resp.Provider) 48 | fmt.Println("Response:", resp.Choices[0].Message.Content) 49 | } 50 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "./Dockerfile", 4 | "context": ".." 5 | }, 6 | "customizations": { 7 | "vscode": { 8 | "extensions": [ 9 | "golang.go", 10 | "denoland.vscode-deno", 11 | "mhutchie.git-graph", 12 | "mutantdino.resourcemonitor", 13 | "Compile-TomaszKasperczyk.copy-to-llm", 14 | "donjayamanne.githistory", 15 | "task.vscode-task" 16 | ], 17 | "settings": { 18 | "deno.enable": true, 19 | "editor.formatOnSave": true, 20 | "editor.foldingStrategy": "indentation", 21 | "files.eol": "\n", 22 | "[json]": { 23 | "editor.defaultFormatter": "denoland.vscode-deno" 24 | }, 25 | "[jsonc]": { 26 | "editor.defaultFormatter": "denoland.vscode-deno" 27 | }, 28 | "[typescript]": { 29 | "editor.defaultFormatter": "denoland.vscode-deno" 30 | }, 31 | "[javascript]": { 32 | "editor.defaultFormatter": "denoland.vscode-deno" 33 | }, 34 | "[css]": { 35 | "editor.defaultFormatter": "denoland.vscode-deno" 36 | }, 37 | "[html]": { 38 | "editor.defaultFormatter": "denoland.vscode-deno" 39 | }, 40 | "[markdown]": { 41 | "editor.defaultFormatter": "denoland.vscode-deno" 42 | }, 43 | "[yaml]": { 44 | "editor.defaultFormatter": "denoland.vscode-deno" 45 | }, 46 | "[go]": { 47 | "editor.defaultFormatter": "golang.go" 48 | }, 49 | "go.lintTool": "golangci-lint", 50 | "go.lintFlags": [ 51 | "--fast" 52 | ], 53 | "copyToLLM.extensions": [ 54 | ".go", 55 | ".md", 56 | ".sh", 57 | ".json", 58 | ".yaml", 59 | ".yml" 60 | ] 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /examples/05-function-calling/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/eduardolat/openroutergo" 9 | ) 10 | 11 | // This example demonstrates how to use a model that supports tools to get the weather 12 | // of a city and continue the conversation using the tool's response. 13 | // 14 | // This requires a model that supports tools, you can find the list here: 15 | // https://openrouter.ai/models?supported_parameters=tools&order=top-weekly 16 | // 17 | // You can copy this code modify the api key, model, and run it. 18 | 19 | const apiKey = "sk......." 20 | const model = "google/gemini-2.0-flash-exp:free" 21 | 22 | func getWeather(city string) string { 23 | // This is a fake function that returns a string but you can 24 | // do calculations, api calls, database queries, etc. 25 | return "It's cold and -120 celsius degrees in " + city + " right now. Literally freezing." 26 | } 27 | 28 | func main() { 29 | client, err := openroutergo.NewClient().WithAPIKey(apiKey).Create() 30 | if err != nil { 31 | log.Fatalf("Failed to create client: %v", err) 32 | } 33 | 34 | completion := client. 35 | NewChatCompletion(). 36 | WithDebug(true). // Enable debug mode to see the request and response in the console 37 | WithModel(model). // Change the model if you want 38 | WithTool(openroutergo.ChatCompletionTool{ 39 | Name: "getWeather", 40 | Description: "Get the weather of a city, use this every time the user asks for the weather", 41 | Parameters: map[string]any{ 42 | // The parameters definition should be a JSON object 43 | "type": "object", 44 | "properties": map[string]any{ 45 | "city": map[string]any{ 46 | "type": "string", 47 | }, 48 | }, 49 | "required": []string{"city"}, 50 | }, 51 | }). 52 | WithSystemMessage("You are a helpful assistant expert in geography."). 53 | WithUserMessage("I want to know the weather in the capital of Brazil and a joke about it") 54 | 55 | completion, resp, err := completion.Execute() 56 | if err != nil { 57 | log.Fatalf("Failed to execute completion: %v", err) 58 | } 59 | 60 | if !resp.HasChoices() || !resp.Choices[0].Message.HasToolCalls() { 61 | log.Fatalf("No tool calls returned") 62 | } 63 | 64 | toolCall := resp.Choices[0].Message.ToolCalls[0] 65 | toolName := toolCall.Function.Name 66 | if toolName != "getWeather" { 67 | log.Fatalf("Unexpected tool name: %s", toolName) 68 | } 69 | 70 | toolCallArguments := toolCall.Function.Arguments 71 | args := map[string]any{} 72 | if err := json.Unmarshal([]byte(toolCallArguments), &args); err != nil { 73 | log.Fatalf("Failed to unmarshal tool call arguments: %v", err) 74 | } 75 | 76 | // Call the function with the arguments provided by the model 77 | weather := getWeather(args["city"].(string)) 78 | 79 | // Use the tool response to continue the conversation 80 | _, resp, err = completion. 81 | WithToolMessage(toolCall, weather). 82 | Execute() 83 | if err != nil { 84 | log.Fatalf("Failed to execute completion: %v", err) 85 | } 86 | 87 | fmt.Println("Response:", resp.Choices[0].Message.Content) 88 | } 89 | -------------------------------------------------------------------------------- /chat_completion_message.go: -------------------------------------------------------------------------------- 1 | package openroutergo 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/orsinium-labs/enum" 7 | ) 8 | 9 | type ChatCompletionMessage struct { 10 | // The ID of the tool call, if the message is a tool call. 11 | ToolCallID string `json:"tool_call_id,omitempty,omitzero"` 12 | // The name of the entity that sent the message or the name of the tool, if the message is a tool call. 13 | Name string `json:"name,omitempty,omitzero"` 14 | // Who the message is from. Must be one of openroutergo.RoleSystem, openroutergo.RoleUser, or openroutergo.RoleAssistant. 15 | Role chatCompletionRole `json:"role"` 16 | // The content of the message 17 | Content string `json:"content"` 18 | // When the model decided to call a tool 19 | ToolCalls []ChatCompletionMessageToolCall `json:"tool_calls,omitempty,omitzero"` 20 | } 21 | 22 | // HasToolCalls returns true if the message has tool calls. 23 | func (c ChatCompletionMessage) HasToolCalls() bool { 24 | return len(c.ToolCalls) > 0 25 | } 26 | 27 | // chatCompletionRole is an enum for the role of a message in a chat completion. 28 | type chatCompletionRole enum.Member[string] 29 | 30 | // MarshalJSON implements the json.Marshaler interface for chatCompletionRole. 31 | func (ccr chatCompletionRole) MarshalJSON() ([]byte, error) { 32 | return json.Marshal(ccr.Value) 33 | } 34 | 35 | // UnmarshalJSON implements the json.Unmarshaler interface for chatCompletionRole. 36 | func (ccr *chatCompletionRole) UnmarshalJSON(data []byte) error { 37 | var value string 38 | if err := json.Unmarshal(data, &value); err != nil { 39 | return err 40 | } 41 | 42 | *ccr = chatCompletionRole{Value: value} 43 | return nil 44 | } 45 | 46 | var ( 47 | // RoleSystem is the role of a system message in a chat completion. 48 | RoleSystem = chatCompletionRole{"system"} 49 | // RoleDeveloper is the role of a developer message in a chat completion. 50 | RoleDeveloper = chatCompletionRole{"developer"} 51 | // RoleUser is the role of a user message in a chat completion. 52 | RoleUser = chatCompletionRole{"user"} 53 | // RoleAssistant is the role of an assistant message in a chat completion. 54 | RoleAssistant = chatCompletionRole{"assistant"} 55 | // RoleTool is the role of a tool message in a chat completion. 56 | RoleTool = chatCompletionRole{"tool"} 57 | ) 58 | 59 | type ChatCompletionMessageToolCall struct { 60 | // The ID of the tool call. 61 | ID string `json:"id"` 62 | // The type of tool call. Always "function". 63 | Type string `json:"type"` 64 | // Function is the function that the model wants to call. 65 | Function ChatCompletionMessageToolCallFunction `json:"function,omitempty,omitzero"` 66 | } 67 | 68 | type ChatCompletionMessageToolCallFunction struct { 69 | // The name of the function to call. 70 | Name string `json:"name"` 71 | // The arguments to call the function with, as generated by the model in JSON 72 | // format. Note that the model does not always generate valid JSON, and may 73 | // hallucinate parameters not defined by your function schema. Validate the 74 | // arguments in your code before calling your function. 75 | // 76 | // You have to unmarshal the arguments to the correct type yourself. 77 | Arguments string `json:"arguments"` 78 | } 79 | -------------------------------------------------------------------------------- /internal/optional/types.go: -------------------------------------------------------------------------------- 1 | package optional 2 | 3 | import "encoding/json" 4 | 5 | // Optional is a type that represents an optional value of any type. 6 | // 7 | // It is used to represent a value that may or may not be set. 8 | // 9 | // If IsSet is false, the Value is not set. 10 | // If IsSet is true, the Value is set. 11 | type Optional[T any] struct { 12 | IsSet bool 13 | Value T 14 | } 15 | 16 | func (o Optional[T]) IsZero() bool { 17 | return !o.IsSet 18 | } 19 | 20 | func (o Optional[T]) MarshalJSON() ([]byte, error) { 21 | if !o.IsSet { 22 | return []byte("null"), nil 23 | } 24 | return json.Marshal(o.Value) 25 | } 26 | 27 | func (o *Optional[T]) UnmarshalJSON(b []byte) error { 28 | var value T 29 | 30 | if len(b) == 0 || string(b) == "null" { 31 | o.IsSet = false 32 | o.Value = value 33 | return nil 34 | } 35 | 36 | if err := json.Unmarshal(b, &value); err != nil { 37 | return err 38 | } 39 | 40 | o.IsSet = true 41 | o.Value = value 42 | return nil 43 | } 44 | 45 | // String is an optional string. 46 | // 47 | // It is used to represent a string that may or may not be set. 48 | // 49 | // If IsSet is false, the Value is not set. 50 | // If IsSet is true, the Value is set. 51 | type String = Optional[string] 52 | 53 | // Int is an optional int. 54 | // 55 | // It is used to represent an int that may or may not be set. 56 | // 57 | // If IsSet is false, the Value is not set. 58 | // If IsSet is true, the Value is set. 59 | type Int = Optional[int] 60 | 61 | // Float64 is an optional float64. 62 | // 63 | // It is used to represent a float64 that may or may not be set. 64 | // 65 | // If IsSet is false, the Value is not set. 66 | // If IsSet is true, the Value is set. 67 | type Float64 = Optional[float64] 68 | 69 | // Bool is an optional bool. 70 | // 71 | // It is used to represent a bool that may or may not be set. 72 | // 73 | // If IsSet is false, the Value is not set. 74 | // If IsSet is true, the Value is set. 75 | type Bool = Optional[bool] 76 | 77 | // Any is an optional any type. 78 | // 79 | // It is used to represent a value of any type that may or may not be set. 80 | // 81 | // If IsSet is false, the Value is not set. 82 | // If IsSet is true, the Value is set. 83 | type Any = Optional[any] 84 | 85 | // MapStringAny is an optional map of string to any type. 86 | // 87 | // It is used to represent a map of string to any type that may or may not be set. 88 | // 89 | // If IsSet is false, the Value is not set. 90 | // If IsSet is true, the Value is set. 91 | type MapStringAny = Optional[map[string]any] 92 | 93 | // MapStringString is an optional map of string to string. 94 | // 95 | // It is used to represent a map of string to string that may or may not be set. 96 | // 97 | // If IsSet is false, the Value is not set. 98 | // If IsSet is true, the Value is set. 99 | type MapStringString = Optional[map[string]string] 100 | 101 | // MapIntInt is an optional map of int to int. 102 | // 103 | // It is used to represent a map of int to int that may or may not be set. 104 | // 105 | // If IsSet is false, the Value is not set. 106 | // If IsSet is true, the Value is set. 107 | type MapIntInt = Optional[map[int]int] 108 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # To make sure we have the deno and golang binaries 2 | FROM denoland/deno:debian-2.2.3 AS deno 3 | FROM golang:1.22.12-bookworm AS golang 4 | 5 | # Set the base image 6 | FROM debian:12.9 7 | 8 | # Declare ARG to make it available in the RUN commands 9 | ARG TARGETPLATFORM 10 | RUN echo "Building for ${TARGETPLATFORM}" 11 | RUN if [ "${TARGETPLATFORM}" != "linux/amd64" ] && [ "${TARGETPLATFORM}" != "linux/arm64" ]; then \ 12 | echo "Unsupported architecture: ${TARGETPLATFORM}" && \ 13 | exit 1; \ 14 | fi 15 | 16 | # Set the general environment variables, and move to temp dir 17 | ENV DEBIAN_FRONTEND="noninteractive" 18 | ENV PATH="$PATH:/usr/local/go/bin" 19 | ENV PATH="$PATH:/usr/local/dl/bin" 20 | ENV GOBIN="/usr/local/go/bin" 21 | RUN mkdir -p /app/temp /usr/local/dl/bin 22 | WORKDIR /app/temp 23 | 24 | # Install deno from docker image 25 | COPY --from=deno /usr/bin/deno /usr/local/bin/deno 26 | 27 | # Install golang from docker image 28 | COPY --from=golang /usr/local/go /usr/local/go 29 | 30 | # Install system dependencies 31 | RUN apt update && \ 32 | apt install -y wget curl zip unzip p7zip-full tzdata git && \ 33 | rm -rf /var/lib/apt/lists/* 34 | 35 | # Install downloadable binaries 36 | RUN set -e && \ 37 | if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ 38 | echo "Downloading arm64 binaries" && \ 39 | # Install task 40 | wget --no-verbose https://github.com/go-task/task/releases/download/v3.41.0/task_linux_arm64.tar.gz && \ 41 | tar -xzf task_linux_arm64.tar.gz && \ 42 | mv ./task /usr/local/dl/bin/task && \ 43 | # Install golangci-lint 44 | wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.64.6/golangci-lint-1.64.6-linux-arm64.tar.gz && \ 45 | tar -xzf golangci-lint-1.64.6-linux-arm64.tar.gz && \ 46 | mv ./golangci-lint-1.64.6-linux-arm64/golangci-lint /usr/local/dl/bin/golangci-lint; \ 47 | else \ 48 | echo "Downloading amd64 binaries" && \ 49 | # Install task 50 | wget --no-verbose https://github.com/go-task/task/releases/download/v3.41.0/task_linux_amd64.tar.gz && \ 51 | tar -xzf task_linux_amd64.tar.gz && \ 52 | mv ./task /usr/local/dl/bin/task && \ 53 | # Install golangci-lint 54 | wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.64.6/golangci-lint-1.64.6-linux-amd64.tar.gz && \ 55 | tar -xzf golangci-lint-1.64.6-linux-amd64.tar.gz && \ 56 | mv ./golangci-lint-1.64.6-linux-amd64/golangci-lint /usr/local/dl/bin/golangci-lint; \ 57 | fi && \ 58 | # Make binaries executable 59 | chmod +x /usr/local/dl/bin/* 60 | 61 | # Default git config 62 | # https://github.com/golangci/golangci-lint/issues/4033 63 | RUN git config --global --add safe.directory '*' 64 | 65 | # Go to the app dir, delete the temporary dir and create backups dir 66 | WORKDIR /app 67 | RUN rm -rf /app/temp && \ 68 | mkdir /backups && \ 69 | chmod 777 /backups 70 | 71 | ############## 72 | # START HERE # 73 | ############## 74 | 75 | # Add the startup script on every bash session 76 | COPY ./.devcontainer/startup.sh /usr/local/bin/startup.sh 77 | RUN echo "\n\n" >> /root/.bashrc && \ 78 | cat /usr/local/bin/startup.sh >> /root/.bashrc 79 | 80 | # Command just to keep the container running 81 | CMD ["sleep", "infinity"] 82 | -------------------------------------------------------------------------------- /examples/07-force-response-format/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/eduardolat/openroutergo" 9 | ) 10 | 11 | // This example demonstrates how to use JSON Schema Mode to ensure that the model's response 12 | // matches a specific JSON schema. This guarantees that the response is not only valid JSON but 13 | // also adheres to the defined structure. 14 | // 15 | // You can copy this code modify the api key, model, and run it. 16 | 17 | const apiKey = "sk......." 18 | const model = "google/gemini-2.0-flash-exp:free" 19 | 20 | func main() { 21 | client, err := openroutergo. 22 | NewClient(). 23 | WithAPIKey(apiKey). 24 | WithRefererURL("https://my-app.com"). // Optional, for rankings on openrouter.ai 25 | WithRefererTitle("My App"). // Optional, for rankings on openrouter.ai 26 | Create() 27 | if err != nil { 28 | log.Fatalf("Failed to create client: %v", err) 29 | } 30 | 31 | completion := client. 32 | NewChatCompletion(). 33 | WithDebug(true). // Enable debug mode to see the request and response in the console 34 | WithModel(model). // Change the model if you want 35 | 36 | // The following is the basic JSON Mode that only guarantees the message the model 37 | // generates is valid JSON but it doesn't guarantee that the JSON matches 38 | // any specific schema. 👇 39 | // 40 | // WithResponseFormat(map[string]any{"type": "json_object"}). 41 | // 42 | // -------------------------------------------------------------------------------- 43 | // 44 | // However, if you want to guarantee that the JSON matches a specific schema, you 45 | // can use the JSON Schema Mode. 👇 46 | WithResponseFormat(map[string]any{ 47 | "type": "json_schema", 48 | "json_schema": map[string]any{ 49 | "name": "capital_response", 50 | "schema": map[string]any{ 51 | "type": "object", 52 | "properties": map[string]any{ 53 | "country": map[string]any{ 54 | "type": "string", 55 | }, 56 | "capital": map[string]any{ 57 | "type": "string", 58 | }, 59 | "curious_fact": map[string]any{ 60 | "type": "string", 61 | }, 62 | }, 63 | "required": []string{"country", "capital", "curious_fact"}, 64 | }, 65 | }, 66 | }). 67 | WithSystemMessage("You are a helpful assistant expert in geography."). 68 | WithUserMessage("What is the capital of France?") 69 | 70 | _, resp, err := completion.Execute() 71 | if err != nil { 72 | log.Fatalf("Failed to execute completion: %v", err) 73 | } 74 | 75 | // You can unmarshal the response to a struct because the model 76 | // should return a valid JSON and match the provided schema. 77 | // 78 | // However, is recommended to validate the response yourself to 79 | // avoid surprises, remember that the model can hallucinate. 80 | var myResponse struct { 81 | Country string `json:"country"` 82 | Capital string `json:"capital"` 83 | CuriousFact string `json:"curious_fact"` 84 | } 85 | if err := json.Unmarshal([]byte(resp.Choices[0].Message.Content), &myResponse); err != nil { 86 | log.Fatalf("Failed to unmarshal response: %v", err) 87 | } 88 | 89 | fmt.Printf( 90 | "The capital of %s is %s and here's a curious fact: %s\n", 91 | myResponse.Country, 92 | myResponse.Capital, 93 | myResponse.CuriousFact, 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /chat_completion_response.go: -------------------------------------------------------------------------------- 1 | package openroutergo 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/orsinium-labs/enum" 7 | ) 8 | 9 | // chatCompletionFinishReason is an enum for the reason the model stopped generating tokens. 10 | // 11 | // - https://openrouter.ai/docs/api-reference/overview#finish-reason 12 | type chatCompletionFinishReason enum.Member[string] 13 | 14 | // MarshalJSON implements the json.Marshaler interface for chatCompletionFinishReason. 15 | func (cfr chatCompletionFinishReason) MarshalJSON() ([]byte, error) { 16 | return json.Marshal(cfr.Value) 17 | } 18 | 19 | // UnmarshalJSON implements the json.Unmarshaler interface for chatCompletionFinishReason. 20 | func (cfr *chatCompletionFinishReason) UnmarshalJSON(data []byte) error { 21 | var value string 22 | if err := json.Unmarshal(data, &value); err != nil { 23 | return err 24 | } 25 | 26 | *cfr = chatCompletionFinishReason{Value: value} 27 | return nil 28 | } 29 | 30 | var ( 31 | // FinishReasonStop is when the model hit a natural stop point or a provided stop sequence. 32 | FinishReasonStop = chatCompletionFinishReason{"stop"} 33 | // FinishReasonLength is when the maximum number of tokens specified in the request was reached. 34 | FinishReasonLength = chatCompletionFinishReason{"length"} 35 | // FinishReasonContentFilter is when content was omitted due to a flag from our content filters. 36 | FinishReasonContentFilter = chatCompletionFinishReason{"content_filter"} 37 | // FinishReasonToolCalls is when the model called a tool. 38 | FinishReasonToolCalls = chatCompletionFinishReason{"tool_calls"} 39 | // FinishReasonError is when the model returned an error. 40 | FinishReasonError = chatCompletionFinishReason{"error"} 41 | ) 42 | 43 | // ChatCompletionResponse is the response from the OpenRouter API for a chat completion request. 44 | // 45 | // - https://openrouter.ai/docs/api-reference/overview#responses 46 | // - https://platform.openai.com/docs/api-reference/chat/object 47 | type ChatCompletionResponse struct { 48 | // A unique identifier for the chat completion. 49 | ID string `json:"id"` 50 | // A list of chat completion choices (the responses from the model). 51 | Choices []ChatCompletionResponseChoice `json:"choices"` 52 | // Usage statistics for the completion request. 53 | Usage ChatCompletionResponseUsage `json:"usage"` 54 | // The Unix timestamp (in seconds) of when the chat completion was created. 55 | Created int `json:"created"` 56 | // The model used for the chat completion. 57 | Model string `json:"model"` 58 | // The provider used for the chat completion. 59 | Provider string `json:"provider"` 60 | // The object type, which is always "chat.completion" 61 | Object string `json:"object"` 62 | } 63 | 64 | // HasChoices returns true if the chat completion has choices. 65 | func (c ChatCompletionResponse) HasChoices() bool { 66 | return len(c.Choices) > 0 67 | } 68 | 69 | type ChatCompletionResponseChoice struct { 70 | // The reason the model stopped generating tokens. This will be `stop` if the model hit a 71 | // natural stop point or a provided stop sequence, `length` if the maximum number of 72 | // tokens specified in the request was reached, `content_filter` if content was omitted 73 | // due to a flag from our content filters, `tool_calls` if the model called a tool, or 74 | // `error` if the model returned an error. 75 | FinishReason chatCompletionFinishReason `json:"finish_reason"` 76 | // A chat completion message generated by the model. 77 | Message ChatCompletionMessage `json:"message"` 78 | } 79 | 80 | type ChatCompletionResponseUsage struct { 81 | // The number of tokens in the prompt. 82 | PromptTokens int `json:"prompt_tokens"` 83 | // The number of tokens in the generated completion. 84 | CompletionTokens int `json:"completion_tokens"` 85 | // The total number of tokens used in the request (prompt + completion). 86 | TotalTokens int `json:"total_tokens"` 87 | } 88 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package openroutergo 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/eduardolat/openroutergo/internal/optional" 10 | "github.com/eduardolat/openroutergo/internal/strutil" 11 | ) 12 | 13 | const ( 14 | defaultBaseURL = "https://openrouter.ai/api/v1" 15 | defaultTimeout = 3 * time.Minute 16 | ) 17 | 18 | // Client represents a client for the OpenRouter API. 19 | type Client struct { 20 | baseURL string 21 | apiKey optional.String 22 | refererURL optional.String 23 | refererTitle optional.String 24 | httpClient *http.Client 25 | } 26 | 27 | // clientBuilder is a chainable builder for the OpenRouter client. 28 | type clientBuilder struct { 29 | client *Client 30 | } 31 | 32 | // NewClient starts the creation of a new OpenRouter client. 33 | func NewClient() *clientBuilder { 34 | return &clientBuilder{ 35 | client: &Client{ 36 | baseURL: defaultBaseURL, 37 | apiKey: optional.String{IsSet: false}, 38 | refererURL: optional.String{IsSet: false}, 39 | refererTitle: optional.String{IsSet: false}, 40 | httpClient: &http.Client{Timeout: defaultTimeout}, 41 | }, 42 | } 43 | } 44 | 45 | // WithBaseURL sets a custom base URL for the API. 46 | // 47 | // If not set, the default base URL will be used: https://openrouter.ai/api/v1 48 | func (b *clientBuilder) WithBaseURL(baseURL string) *clientBuilder { 49 | b.client.baseURL = strutil.RemoveTrailingSlashes(baseURL) 50 | return b 51 | } 52 | 53 | // WithAPIKey sets the API key for authentication. 54 | func (b *clientBuilder) WithAPIKey(apiKey string) *clientBuilder { 55 | b.client.apiKey = optional.String{IsSet: true, Value: apiKey} 56 | return b 57 | } 58 | 59 | // WithRefererURL sets the referer URL for the API which identifies your app 60 | // and allows it to be tracked and discoverable on OpenRouter. 61 | // 62 | // It uses the `HTTP-Referer` header. 63 | // 64 | // - https://openrouter.ai/docs/api-reference/overview#headers 65 | func (b *clientBuilder) WithRefererURL(refererURL string) *clientBuilder { 66 | b.client.refererURL = optional.String{IsSet: true, Value: refererURL} 67 | return b 68 | } 69 | 70 | // WithRefererTitle sets the referer title for the API which identifies your app 71 | // and allows it to be discoverable on OpenRouter. 72 | // 73 | // It uses the `X-Title` header. 74 | // 75 | // - https://openrouter.ai/docs/api-reference/overview#headers 76 | func (b *clientBuilder) WithRefererTitle(refererTitle string) *clientBuilder { 77 | b.client.refererTitle = optional.String{IsSet: true, Value: refererTitle} 78 | return b 79 | } 80 | 81 | // WithHTTPClient sets a custom HTTP client for the API, this allows setting 82 | // a custom timeout, proxy, etc. 83 | // 84 | // If not set, the default HTTP client will be used. 85 | func (b *clientBuilder) WithHTTPClient(httpClient *http.Client) *clientBuilder { 86 | b.client.httpClient = httpClient 87 | return b 88 | } 89 | 90 | // WithTimeout sets a custom timeout for the HTTP client to be used for all requests. 91 | // 92 | // If not set, the default timeout of 3 minutes will be used. 93 | func (b *clientBuilder) WithTimeout(timeout time.Duration) *clientBuilder { 94 | if b.client.httpClient == nil { 95 | b.client.httpClient = &http.Client{} 96 | } 97 | 98 | b.client.httpClient.Timeout = timeout 99 | return b 100 | } 101 | 102 | // Create builds and returns the OpenRouter client. 103 | func (b *clientBuilder) Create() (*Client, error) { 104 | if b.client.baseURL == "" { 105 | return nil, ErrBaseURLRequired 106 | } 107 | 108 | if !b.client.apiKey.IsSet { 109 | return nil, ErrAPIKeyRequired 110 | } 111 | 112 | return b.client, nil 113 | } 114 | 115 | // newRequest creates a new request for the OpenRouter API, it sets the 116 | // necessary headers and adds the API key to the request. 117 | func (c *Client) newRequest(ctx context.Context, method string, path string, body []byte) (*http.Request, error) { 118 | url := strutil.CreateEndpoint(c.baseURL, path) 119 | req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body)) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | req.Header.Set("Content-Type", "application/json") 125 | req.Header.Set("Authorization", "Bearer "+c.apiKey.Value) 126 | if c.refererURL.IsSet { 127 | req.Header.Set("HTTP-Referer", c.refererURL.Value) 128 | } 129 | if c.refererTitle.IsSet { 130 | req.Header.Set("X-Title", c.refererTitle.Value) 131 | } 132 | 133 | return req, nil 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenRouterGo 2 | 3 | A powerful, developer-friendly Go SDK for 4 | [OpenRouter.ai](https://openrouter.ai) - the platform that gives you unified 5 | access to 100+ AI models from OpenAI, Anthropic, Google, and more through a 6 | single consistent API. 7 | 8 |

9 | 10 | Go Reference 11 | 12 | 13 | Go Report Card 14 | 15 | 16 | Release Version 17 | 18 | 19 | License 20 | 21 | 22 | 23 | 24 |

25 | 26 | > [!WARNING] 27 | > This client is not yet stable and the API signature may change in the future 28 | > until it reaches version 1.0.0, so be careful when upgrading. However, the API 29 | > signature should not change too much. 30 | 31 | ## Features 32 | 33 | - 🚀 **Simple & Intuitive API** - Fluent builder pattern with method chaining 34 | for clean, readable code 35 | - 🔄 **Smart Fallbacks** - Automatically retry with alternative models if your 36 | first choice fails or is rate-limited 37 | - 🛠️ **Function Calling** - Let AI models access your tools and functions when 38 | needed 39 | - 📊 **Structured Outputs** - Force responses in valid JSON format with schema 40 | validation 41 | - 🧠 **Complete Control** - Fine-tune model behavior with temperature, top-p, 42 | frequency penalty and more 43 | - 🔍 **Debug Mode** - Instantly see the exact requests and responses for easier 44 | development 45 | 46 | ## Installation 47 | 48 | Go version 1.22 or higher is required. 49 | 50 | ```bash 51 | go get github.com/eduardolat/openroutergo 52 | ``` 53 | 54 | ## Quick Start Example 55 | 56 | ```go 57 | package main 58 | 59 | import ( 60 | "fmt" 61 | "log" 62 | 63 | "github.com/eduardolat/openroutergo" 64 | ) 65 | 66 | // Replace with your own API key and the model you want to use 67 | const apiKey = "sk......." 68 | const model = "deepseek/deepseek-r1:free" 69 | 70 | func main() { 71 | // Create a client with your API key 72 | client, err := openroutergo. 73 | NewClient(). 74 | WithAPIKey(apiKey). 75 | Create() 76 | if err != nil { 77 | log.Fatalf("Failed to create client: %v", err) 78 | } 79 | 80 | // Build and execute your request with a fluent API 81 | completion, resp, err := client. 82 | NewChatCompletion(). 83 | WithModel(model). 84 | WithSystemMessage("You are a helpful assistant expert in geography."). 85 | WithUserMessage("What is the capital of France?"). 86 | Execute() 87 | if err != nil { 88 | log.Fatalf("Failed to execute completion: %v", err) 89 | } 90 | 91 | // Print the model's first response 92 | fmt.Println("Response:", resp.Choices[0].Message.Content) 93 | 94 | // Continue the conversation seamlessly, the last response is 95 | // automatically added to the conversation history 96 | _, resp, err = completion. 97 | WithUserMessage("That's great! What about Japan?"). 98 | Execute() 99 | if err != nil { 100 | log.Fatalf("Failed to execute completion: %v", err) 101 | } 102 | 103 | // Print the model's second response 104 | fmt.Println("Response:", resp.Choices[0].Message.Content) 105 | } 106 | ``` 107 | 108 | ## More Examples 109 | 110 | We've included several examples to help you get started quickly: 111 | 112 | - [Basic Usage](examples/01-basic/main.go) - Simple chat completion with any 113 | model 114 | - [Clone Completion](examples/02-clone-completion/main.go) - Reuse 115 | configurations for multiple requests 116 | - [Conversation](examples/03-reuse-completion/main.go) - Build multi-turn 117 | conversations with context 118 | - [Model Fallbacks](examples/04-model-fallback/main.go) - Gracefully handle rate 119 | limits and save money with alternative models 120 | - [Function Calling](examples/05-function-calling/main.go) - Allow AI to call 121 | your application functions 122 | - [Advanced Options](examples/06-other-options/main.go) - Explore additional 123 | parameters for fine-tuning 124 | - [JSON Responses](examples/07-force-response-format/main.go) - Get structured, 125 | validated outputs 126 | 127 | ## Get Started 128 | 129 | 1. Get your API key from [OpenRouter.ai](https://openrouter.ai/keys) 130 | 2. Install the package: `go get github.com/eduardolat/openroutergo` 131 | 3. Start building with the examples above 132 | 133 | ## About me 134 | 135 | I'm Eduardo, if you like my work please ⭐ star the repo and find me on the 136 | following platforms: 137 | 138 | - [X](https://x.com/eduardoolat) 139 | - [GitHub](https://github.com/eduardolat) 140 | - [LinkedIn](https://www.linkedin.com/in/eduardolat) 141 | - [My Website](https://eduardo.lat) 142 | - [Buy me a coffee](https://buymeacoffee.com/eduardolat) 143 | 144 | ## License 145 | 146 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file 147 | for details. 148 | -------------------------------------------------------------------------------- /internal/optional/types_test.go: -------------------------------------------------------------------------------- 1 | package optional 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/eduardolat/openroutergo/internal/assert" 8 | ) 9 | 10 | func TestOptionalGenericType(t *testing.T) { 11 | t.Run("MarshalJSON", func(t *testing.T) { 12 | // Test unset value 13 | unset := &Optional[string]{IsSet: false} 14 | data, err := unset.MarshalJSON() 15 | assert.NoError(t, err) 16 | assert.Equal(t, "null", string(data)) 17 | 18 | // Test set value 19 | set := &Optional[string]{IsSet: true, Value: "hello"} 20 | data, err = set.MarshalJSON() 21 | assert.NoError(t, err) 22 | assert.Equal(t, `"hello"`, string(data)) 23 | }) 24 | 25 | t.Run("UnmarshalJSON", func(t *testing.T) { 26 | // Test null value 27 | var opt Optional[int] 28 | err := opt.UnmarshalJSON([]byte("null")) 29 | assert.NoError(t, err) 30 | assert.False(t, opt.IsSet) 31 | 32 | // Test valid value 33 | err = opt.UnmarshalJSON([]byte("42")) 34 | assert.NoError(t, err) 35 | assert.True(t, opt.IsSet) 36 | assert.Equal(t, 42, opt.Value) 37 | }) 38 | } 39 | 40 | func TestDerivedTypes(t *testing.T) { 41 | t.Run("String", func(t *testing.T) { 42 | // Marshal then unmarshal to verify full cycle 43 | original := String{IsSet: true, Value: "test"} 44 | data, err := json.Marshal(&original) 45 | assert.NoError(t, err) 46 | 47 | // Unmarshal 48 | var result String 49 | err = json.Unmarshal(data, &result) 50 | assert.NoError(t, err) 51 | assert.True(t, result.IsSet) 52 | assert.Equal(t, "test", result.Value) 53 | }) 54 | 55 | t.Run("Int", func(t *testing.T) { 56 | // Test int with null value 57 | var num Int 58 | err := json.Unmarshal([]byte("null"), &num) 59 | assert.NoError(t, err) 60 | assert.False(t, num.IsSet) 61 | 62 | // Test with value 63 | err = json.Unmarshal([]byte("99"), &num) 64 | assert.NoError(t, err) 65 | assert.True(t, num.IsSet) 66 | assert.Equal(t, 99, num.Value) 67 | 68 | // Marshal 69 | data, err := json.Marshal(&num) 70 | assert.NoError(t, err) 71 | assert.Equal(t, `99`, string(data)) 72 | }) 73 | 74 | t.Run("Bool", func(t *testing.T) { 75 | // Testing marshal/unmarshal false value 76 | original := Bool{IsSet: true, Value: false} 77 | data, err := json.Marshal(original) 78 | assert.NoError(t, err) 79 | 80 | var result Bool 81 | err = json.Unmarshal(data, &result) 82 | assert.NoError(t, err) 83 | assert.True(t, result.IsSet) 84 | assert.False(t, result.Value) 85 | }) 86 | 87 | t.Run("Float64", func(t *testing.T) { 88 | // Test marshal 89 | original := Float64{IsSet: true, Value: 3.14} 90 | data, err := json.Marshal(original) 91 | assert.NoError(t, err) 92 | assert.Equal(t, `3.14`, string(data)) 93 | 94 | // Test float64 with null value 95 | var num Float64 96 | err = json.Unmarshal([]byte("null"), &num) 97 | assert.NoError(t, err) 98 | assert.False(t, num.IsSet) 99 | 100 | // Test with value 101 | err = json.Unmarshal([]byte("3.14"), &num) 102 | assert.NoError(t, err) 103 | assert.True(t, num.IsSet) 104 | assert.Equal(t, 3.14, num.Value) 105 | }) 106 | 107 | t.Run("Any", func(t *testing.T) { 108 | // Test anyVar with null value 109 | var anyVar Any 110 | err := json.Unmarshal([]byte("null"), &anyVar) 111 | assert.NoError(t, err) 112 | assert.False(t, anyVar.IsSet) 113 | 114 | // Test anyVar with string 115 | err = json.Unmarshal([]byte(`"test"`), &anyVar) 116 | assert.NoError(t, err) 117 | assert.True(t, anyVar.IsSet) 118 | 119 | // Test with value 120 | err = json.Unmarshal([]byte("{\"key\": \"value\"}"), &anyVar) 121 | assert.NoError(t, err) 122 | assert.True(t, anyVar.IsSet) 123 | anyMap := anyVar.Value.(map[string]interface{}) 124 | assert.Equal(t, "value", anyMap["key"]) 125 | 126 | // Marshal test 127 | data, err := json.Marshal(&anyVar) 128 | assert.NoError(t, err) 129 | assert.Equal(t, `{"key":"value"}`, string(data)) 130 | }) 131 | 132 | t.Run("MapStringAny", func(t *testing.T) { 133 | // Test map[string]any with null value 134 | var mapAny MapStringAny 135 | err := json.Unmarshal([]byte("null"), &mapAny) 136 | assert.NoError(t, err) 137 | assert.False(t, mapAny.IsSet) 138 | 139 | // Test with value 140 | err = json.Unmarshal([]byte("{\"key\": \"value\"}"), &mapAny) 141 | assert.NoError(t, err) 142 | assert.True(t, mapAny.IsSet) 143 | mapAnyMap := mapAny.Value 144 | assert.Equal(t, "value", mapAnyMap["key"]) 145 | 146 | // Marshal test 147 | data, err := json.Marshal(&mapAny) 148 | assert.NoError(t, err) 149 | assert.Equal(t, `{"key":"value"}`, string(data)) 150 | }) 151 | 152 | t.Run("MapStringString", func(t *testing.T) { 153 | // Test map[string]string with null value 154 | var mapString MapStringString 155 | err := json.Unmarshal([]byte("null"), &mapString) 156 | assert.NoError(t, err) 157 | assert.False(t, mapString.IsSet) 158 | 159 | // Test with value 160 | err = json.Unmarshal([]byte("{\"key\": \"value\"}"), &mapString) 161 | assert.NoError(t, err) 162 | assert.True(t, mapString.IsSet) 163 | mapStringMap := mapString.Value 164 | assert.Equal(t, "value", mapStringMap["key"]) 165 | 166 | // Marshal test 167 | data, err := json.Marshal(&mapString) 168 | assert.NoError(t, err) 169 | assert.Equal(t, `{"key":"value"}`, string(data)) 170 | }) 171 | 172 | t.Run("MapIntInt", func(t *testing.T) { 173 | // Test map[int]int with null value 174 | var mapInt MapIntInt 175 | err := json.Unmarshal([]byte("null"), &mapInt) 176 | assert.NoError(t, err) 177 | assert.False(t, mapInt.IsSet) 178 | 179 | // Test with value 180 | err = json.Unmarshal([]byte("{\"1\": 2, \"3\": 4}"), &mapInt) 181 | assert.NoError(t, err) 182 | assert.True(t, mapInt.IsSet) 183 | mapIntMap := mapInt.Value 184 | assert.Equal(t, 2, mapIntMap[1]) 185 | assert.Equal(t, 4, mapIntMap[3]) 186 | 187 | // Marshal test 188 | data, err := json.Marshal(&mapInt) 189 | assert.NoError(t, err) 190 | assert.Equal(t, `{"1":2,"3":4}`, string(data)) 191 | }) 192 | } 193 | 194 | func TestEmptyStringBehavior(t *testing.T) { 195 | // This test is critical because empty string isn't valid JSON 196 | t.Run("Direct vs json.Marshal", func(t *testing.T) { 197 | opt := &Optional[string]{IsSet: false} 198 | 199 | // Direct call returns empty string 200 | direct, err := opt.MarshalJSON() 201 | assert.NoError(t, err) 202 | assert.Equal(t, "null", string(direct)) 203 | 204 | // json.Marshal might handle it differently 205 | _, err = json.Marshal(opt) 206 | assert.NoError(t, err) 207 | }) 208 | } 209 | 210 | func TestRealWorldUsage(t *testing.T) { 211 | // Test with a realistic struct 212 | type User struct { 213 | Name string `json:"name"` 214 | Age Int `json:"age"` 215 | Email String `json:"email,omitempty"` 216 | Verified Bool `json:"verified,omitempty"` 217 | } 218 | 219 | t.Run("Complete serialization cycle", func(t *testing.T) { 220 | original := User{ 221 | Name: "Jane", 222 | Age: Int{IsSet: true, Value: 30}, 223 | Email: String{IsSet: true, Value: "jane@example.com"}, 224 | Verified: Bool{IsSet: false}, 225 | } 226 | 227 | data, err := json.Marshal(original) 228 | assert.NoError(t, err) 229 | 230 | var result User 231 | err = json.Unmarshal(data, &result) 232 | assert.NoError(t, err) 233 | 234 | assert.Equal(t, original.Name, result.Name) 235 | assert.Equal(t, original.Age.Value, result.Age.Value) 236 | assert.True(t, result.Age.IsSet) 237 | assert.Equal(t, original.Email.Value, result.Email.Value) 238 | assert.True(t, result.Email.IsSet) 239 | assert.False(t, result.Verified.IsSet) 240 | }) 241 | } 242 | -------------------------------------------------------------------------------- /chat_completion.go: -------------------------------------------------------------------------------- 1 | package openroutergo 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "slices" 10 | "sync" 11 | 12 | "github.com/eduardolat/openroutergo/internal/debug" 13 | "github.com/eduardolat/openroutergo/internal/optional" 14 | ) 15 | 16 | // NewChatCompletion creates a new chat completion request builder for the OpenRouter API. 17 | // 18 | // Docs: 19 | // - Reference: https://openrouter.ai/docs/api-reference/chat-completion 20 | // - Request: https://openrouter.ai/docs/api-reference/overview#completions-request-format 21 | // - Parameters: https://openrouter.ai/docs/api-reference/parameters 22 | // - Response: https://openrouter.ai/docs/api-reference/overview#completionsresponse-format 23 | func (c *Client) NewChatCompletion() *chatCompletionBuilder { 24 | return &chatCompletionBuilder{ 25 | client: c, 26 | mu: sync.Mutex{}, 27 | executing: false, 28 | debug: false, 29 | ctx: context.Background(), 30 | model: optional.String{IsSet: false}, 31 | fallbackModels: []string{}, 32 | messages: []ChatCompletionMessage{}, 33 | temperature: optional.Float64{IsSet: false}, 34 | topP: optional.Float64{IsSet: false}, 35 | topK: optional.Int{IsSet: false}, 36 | frequencyPenalty: optional.Float64{IsSet: false}, 37 | presencePenalty: optional.Float64{IsSet: false}, 38 | repetitionPenalty: optional.Float64{IsSet: false}, 39 | minP: optional.Float64{IsSet: false}, 40 | topA: optional.Float64{IsSet: false}, 41 | seed: optional.Int{IsSet: false}, 42 | maxTokens: optional.Int{IsSet: false}, 43 | logitBias: optional.MapIntInt{IsSet: false}, 44 | logprobs: optional.Bool{IsSet: false}, 45 | topLogprobs: optional.Int{IsSet: false}, 46 | responseFormat: optional.MapStringAny{IsSet: false}, 47 | structuredOutputs: optional.Bool{IsSet: false}, 48 | stop: []string{}, 49 | tools: []chatCompletionToolFunction{}, 50 | toolChoice: optional.String{IsSet: false}, 51 | maxPromptPrice: optional.Float64{IsSet: false}, 52 | maxCompletionPrice: optional.Float64{IsSet: false}, 53 | } 54 | } 55 | 56 | type chatCompletionBuilder struct { 57 | client *Client 58 | mu sync.Mutex 59 | executing bool 60 | debug bool 61 | ctx context.Context 62 | model optional.String 63 | fallbackModels []string 64 | messages []ChatCompletionMessage 65 | temperature optional.Float64 66 | topP optional.Float64 67 | topK optional.Int 68 | frequencyPenalty optional.Float64 69 | presencePenalty optional.Float64 70 | repetitionPenalty optional.Float64 71 | minP optional.Float64 72 | topA optional.Float64 73 | seed optional.Int 74 | maxTokens optional.Int 75 | logitBias optional.MapIntInt 76 | logprobs optional.Bool 77 | topLogprobs optional.Int 78 | responseFormat optional.MapStringAny 79 | structuredOutputs optional.Bool 80 | stop []string 81 | tools []chatCompletionToolFunction 82 | toolChoice optional.String 83 | maxPromptPrice optional.Float64 84 | maxCompletionPrice optional.Float64 85 | } 86 | 87 | // Clone returns a completely new chat completion builder with the same configuration as the current 88 | // builder. 89 | // 90 | // This is useful if you want to reuse the same configuration for multiple requests. 91 | func (b *chatCompletionBuilder) Clone() *chatCompletionBuilder { 92 | return &chatCompletionBuilder{ 93 | client: b.client, 94 | mu: sync.Mutex{}, 95 | executing: false, 96 | debug: b.debug, 97 | ctx: b.ctx, 98 | messages: b.messages, 99 | model: b.model, 100 | fallbackModels: b.fallbackModels, 101 | temperature: b.temperature, 102 | topP: b.topP, 103 | topK: b.topK, 104 | frequencyPenalty: b.frequencyPenalty, 105 | presencePenalty: b.presencePenalty, 106 | repetitionPenalty: b.repetitionPenalty, 107 | minP: b.minP, 108 | topA: b.topA, 109 | seed: b.seed, 110 | maxTokens: b.maxTokens, 111 | logitBias: b.logitBias, 112 | logprobs: b.logprobs, 113 | topLogprobs: b.topLogprobs, 114 | responseFormat: b.responseFormat, 115 | structuredOutputs: b.structuredOutputs, 116 | stop: b.stop, 117 | tools: b.tools, 118 | toolChoice: b.toolChoice, 119 | maxPromptPrice: b.maxPromptPrice, 120 | maxCompletionPrice: b.maxCompletionPrice, 121 | } 122 | } 123 | 124 | type chatCompletionToolFunction struct { 125 | Type string `json:"type"` // Always "function" 126 | Function ChatCompletionTool `json:"function"` 127 | } 128 | 129 | // ChatCompletionTool is a tool that can be used in a chat completion request. 130 | // 131 | // - Models supporting tool calling: https://openrouter.ai/models?supported_parameters=tools 132 | // - JSON Schema reference: https://json-schema.org/understanding-json-schema/reference 133 | // - Tool calling example: https://platform.openai.com/docs/guides/function-calling 134 | type ChatCompletionTool struct { 135 | // The name of the tool, when the model calls this tool, it will return this name so 136 | // you can identify it. 137 | Name string `json:"name"` 138 | // The description of the tool, make sure to give a good description so the model knows 139 | // when to use it. 140 | Description string `json:"description,omitempty,omitzero"` 141 | // Make sure to define your tool's parameters using map[string]any and following the 142 | // JSON Schema format. 143 | // 144 | // - Format example: https://platform.openai.com/docs/guides/function-calling 145 | // - JSON Schema reference: https://json-schema.org/understanding-json-schema/reference 146 | Parameters map[string]any `json:"parameters"` 147 | } 148 | 149 | // WithDebug sets the debug flag for the chat completion request. 150 | // 151 | // If true, the JSON request and response will be printed to the console for debugging purposes. 152 | func (b *chatCompletionBuilder) WithDebug(debug bool) *chatCompletionBuilder { 153 | b.debug = debug 154 | return b 155 | } 156 | 157 | // WithContext sets the context for the chat completion request. 158 | // 159 | // If not set, a context.Background() context will be used. 160 | func (b *chatCompletionBuilder) WithContext(ctx context.Context) *chatCompletionBuilder { 161 | b.ctx = ctx 162 | return b 163 | } 164 | 165 | // WithModel sets the model for the chat completion request. 166 | // 167 | // If not set, the default model configured in the OpenRouter user's account will be used. 168 | // 169 | // You can search for models here: https://openrouter.ai/models 170 | func (b *chatCompletionBuilder) WithModel(model string) *chatCompletionBuilder { 171 | b.model = optional.String{IsSet: true, Value: model} 172 | return b 173 | } 174 | 175 | // WithModelFallback adds a model to the fallback list for the chat completion request. 176 | // 177 | // You can call this method up to 3 times to add more than one fallback model. 178 | // 179 | // This lets you automatically try other models if the primary model’s providers are down, 180 | // rate-limited, or refuse to reply due to content moderation. 181 | // 182 | // If the primary model is not available, all the fallback models will be tried in the 183 | // same order they were added. 184 | // 185 | // - Docs: https://openrouter.ai/docs/features/model-routing#the-models-parameter 186 | // - Example: https://openrouter.ai/docs/features/model-routing#using-with-openai-sdk 187 | func (b *chatCompletionBuilder) WithModelFallback(modelFallback string) *chatCompletionBuilder { 188 | b.fallbackModels = append(b.fallbackModels, modelFallback) 189 | return b 190 | } 191 | 192 | // WithMessage adds a message to the chat completion request. 193 | // 194 | // All messages are added to the request in the same order they are added. 195 | func (b *chatCompletionBuilder) WithMessage(message ChatCompletionMessage) *chatCompletionBuilder { 196 | b.messages = append(b.messages, message) 197 | return b 198 | } 199 | 200 | // WithSystemMessage adds a system message to the chat completion request. 201 | // 202 | // All messages are added to the request in the same order they are added. 203 | func (b *chatCompletionBuilder) WithSystemMessage(message string) *chatCompletionBuilder { 204 | b.WithMessage(ChatCompletionMessage{Role: RoleSystem, Content: message}) 205 | return b 206 | } 207 | 208 | // WithDeveloperMessage adds a developer message to the chat completion request. 209 | // 210 | // All messages are added to the request in the same order they are added. 211 | func (b *chatCompletionBuilder) WithDeveloperMessage(message string) *chatCompletionBuilder { 212 | b.WithMessage(ChatCompletionMessage{Role: RoleDeveloper, Content: message}) 213 | return b 214 | } 215 | 216 | // WithUserMessage adds a user message to the chat completion request. 217 | // 218 | // If a name is provided, it will be used as the name of the user. 219 | // 220 | // All messages are added to the request in the same order they are added. 221 | func (b *chatCompletionBuilder) WithUserMessage(message string, name ...string) *chatCompletionBuilder { 222 | uname := "" 223 | if len(name) > 0 { 224 | uname = name[0] 225 | } 226 | 227 | b.WithMessage(ChatCompletionMessage{Role: RoleUser, Content: message, Name: uname}) 228 | return b 229 | } 230 | 231 | // WithAssistantMessage adds an assistant message to the chat completion request. 232 | // 233 | // If a name is provided, it will be used as the name of the assistant. 234 | // 235 | // All messages are added to the request in the same order they are added. 236 | func (b *chatCompletionBuilder) WithAssistantMessage(message string, name ...string) *chatCompletionBuilder { 237 | uname := "" 238 | if len(name) > 0 { 239 | uname = name[0] 240 | } 241 | 242 | b.WithMessage(ChatCompletionMessage{Role: RoleAssistant, Content: message, Name: uname}) 243 | return b 244 | } 245 | 246 | // WithToolMessage adds a tool response message to the chat completion request. 247 | // 248 | // When the model asks to use a tool, use this method to send the tool's result 249 | // back to the model and continue the conversation. 250 | // 251 | // All messages are added to the request in the same order they are added. 252 | // 253 | // Arguments: 254 | // - toolCallRequest: The tool request made by the model. This is needed so the model knows 255 | // which tool request this message is responding to. 256 | // - toolResponseContent: The result you got from the tool. This content will be sent 257 | // back to the model. 258 | func (b *chatCompletionBuilder) WithToolMessage(toolCallRequest ChatCompletionMessageToolCall, toolResponseContent string) *chatCompletionBuilder { 259 | b.WithMessage(ChatCompletionMessage{ 260 | Role: RoleTool, 261 | Name: toolCallRequest.Function.Name, 262 | ToolCallID: toolCallRequest.ID, 263 | Content: toolResponseContent, 264 | }) 265 | return b 266 | } 267 | 268 | // WithTemperature sets the temperature for the chat completion request. 269 | // 270 | // This setting influences the variety in the model’s responses. Lower values lead 271 | // to more predictable and typical responses, while higher values encourage more 272 | // diverse and less common responses. At 0, the model always gives the same 273 | // response for a given input. 274 | // 275 | // - Default: 1.0 276 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#temperature 277 | // - Explanation: https://youtu.be/ezgqHnWvua8 278 | func (b *chatCompletionBuilder) WithTemperature(temperature float64) *chatCompletionBuilder { 279 | b.temperature = optional.Float64{IsSet: true, Value: temperature} 280 | return b 281 | } 282 | 283 | // WithTopP sets the top-p value for the chat completion request. 284 | // 285 | // This setting limits the model’s choices to a percentage of likely tokens: only the 286 | // top tokens whose probabilities add up to P. A lower value makes the model’s responses 287 | // more predictable, while the default setting allows for a full range of token choices. 288 | // Think of it like a dynamic Top-K. 289 | // 290 | // - Default: 1.0 291 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#top-p 292 | // - Explanation: https://youtu.be/wQP-im_HInk 293 | func (b *chatCompletionBuilder) WithTopP(topP float64) *chatCompletionBuilder { 294 | b.topP = optional.Float64{IsSet: true, Value: topP} 295 | return b 296 | } 297 | 298 | // WithTopK sets the top-k value for the chat completion request. 299 | // 300 | // This limits the model's choice of tokens at each step, making it choose from 301 | // a smaller set. A value of 1 means the model will always pick the most likely 302 | // next token, leading to predictable results. By default this setting is disabled, 303 | // making the model to consider all choices. 304 | // 305 | // - Default: 0 306 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#top-k 307 | // - Explanation: https://youtu.be/EbZv6-N8Xlk 308 | func (b *chatCompletionBuilder) WithTopK(topK int) *chatCompletionBuilder { 309 | b.topK = optional.Int{IsSet: true, Value: topK} 310 | return b 311 | } 312 | 313 | // WithFrequencyPenalty sets the frequency penalty for the chat completion request. 314 | // 315 | // This setting aims to control the repetition of tokens based on how often they appear 316 | // in the input. It tries to use less frequently those tokens that appear more in the 317 | // input, proportional to how frequently they occur. Token penalty scales with the number 318 | // of occurrences. Negative values will encourage token reuse. 319 | // 320 | // - Default: 0.0 321 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#frequency-penalty 322 | // - Explanation: https://youtu.be/p4gl6fqI0_w 323 | func (b *chatCompletionBuilder) WithFrequencyPenalty(frequencyPenalty float64) *chatCompletionBuilder { 324 | b.frequencyPenalty = optional.Float64{IsSet: true, Value: frequencyPenalty} 325 | return b 326 | } 327 | 328 | // WithPresencePenalty sets the presence penalty for the chat completion request. 329 | // 330 | // Adjusts how often the model repeats specific tokens already used in the input. 331 | // Higher values make such repetition less likely, while negative values do the opposite. 332 | // Token penalty does not scale with the number of occurrences. Negative values will 333 | // encourage token reuse. 334 | // 335 | // - Default: 0.0 336 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#presence-penalty 337 | // - Explanation: https://youtu.be/MwHG5HL-P74 338 | func (b *chatCompletionBuilder) WithPresencePenalty(presencePenalty float64) *chatCompletionBuilder { 339 | b.presencePenalty = optional.Float64{IsSet: true, Value: presencePenalty} 340 | return b 341 | } 342 | 343 | // WithRepetitionPenalty sets the repetition penalty for the chat completion request. 344 | // 345 | // Helps to reduce the repetition of tokens from the input. A higher value makes the 346 | // model less likely to repeat tokens, but too high a value can make the output less 347 | // coherent (often with run-on sentences that lack small words). Token penalty scales 348 | // based on original token's probability. 349 | // 350 | // - Default: 1.0 351 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#repetition-penalty 352 | // - Explanation: https://youtu.be/LHjGAnLm3DM 353 | func (b *chatCompletionBuilder) WithRepetitionPenalty(repetitionPenalty float64) *chatCompletionBuilder { 354 | b.repetitionPenalty = optional.Float64{IsSet: true, Value: repetitionPenalty} 355 | return b 356 | } 357 | 358 | // WithMinP sets the min-p value for the chat completion request. 359 | // 360 | // Represents the minimum probability for a token to be considered, relative to 361 | // the probability of the most likely token. If your Min-P is set to 0.1, that 362 | // means it will only allow for tokens that are at least 1/10th as probable as 363 | // the best possible option. 364 | // 365 | // - Default: 0.0 366 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#min-p 367 | func (b *chatCompletionBuilder) WithMinP(minP float64) *chatCompletionBuilder { 368 | b.minP = optional.Float64{IsSet: true, Value: minP} 369 | return b 370 | } 371 | 372 | // WithTopA sets the top-a value for the chat completion request. 373 | // 374 | // Consider only the top tokens with "sufficiently high" probabilities based on 375 | // the probability of the most likely token. Think of it like a dynamic Top-P. 376 | // A lower Top-A value focuses the choices based on the highest probability token 377 | // but with a narrower scope. 378 | // 379 | // - Default: 0.0 380 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#top-a 381 | func (b *chatCompletionBuilder) WithTopA(topA float64) *chatCompletionBuilder { 382 | b.topA = optional.Float64{IsSet: true, Value: topA} 383 | return b 384 | } 385 | 386 | // WithSeed sets the seed value for the chat completion request. 387 | // 388 | // If specified, the inferencing will sample deterministically, such that repeated 389 | // requests with the same seed and parameters should return the same result. 390 | // Determinism is not guaranteed for some models. 391 | // 392 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#seed 393 | func (b *chatCompletionBuilder) WithSeed(seed int) *chatCompletionBuilder { 394 | b.seed = optional.Int{IsSet: true, Value: seed} 395 | return b 396 | } 397 | 398 | // WithMaxTokens sets the maximum number of tokens to generate for the chat completion request. 399 | // 400 | // This sets the upper limit for the number of tokens the model can generate in response. 401 | // It won't produce more than this limit. The maximum value is the context length minus 402 | // the prompt length. 403 | // 404 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#max-tokens 405 | func (b *chatCompletionBuilder) WithMaxTokens(maxTokens int) *chatCompletionBuilder { 406 | b.maxTokens = optional.Int{IsSet: true, Value: maxTokens} 407 | return b 408 | } 409 | 410 | // WithLogitBias Accepts a JSON object that maps tokens (specified by their token ID in the tokenizer) to 411 | // an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated 412 | // by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should 413 | // decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or 414 | // exclusive selection of the relevant token. 415 | // 416 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#logit-bias 417 | func (b *chatCompletionBuilder) WithLogitBias(logitBias map[int]int) *chatCompletionBuilder { 418 | b.logitBias = optional.MapIntInt{IsSet: true, Value: logitBias} 419 | return b 420 | } 421 | 422 | // WithLogprobs Whether to return log probabilities of the output tokens or not. If true, returns the 423 | // log probabilities of each output token returned. 424 | // 425 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#logprobs 426 | func (b *chatCompletionBuilder) WithLogprobs(logprobs bool) *chatCompletionBuilder { 427 | b.logprobs = optional.Bool{IsSet: true, Value: logprobs} 428 | return b 429 | } 430 | 431 | // WithTopLogprobs An integer between 0 and 20 specifying the number of most likely tokens to return 432 | // at each token position, each with an associated log probability. logprobs must be set to true if 433 | // this parameter is used. 434 | // 435 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#top-logprobs 436 | func (b *chatCompletionBuilder) WithTopLogprobs(topLogprobs int) *chatCompletionBuilder { 437 | b.topLogprobs = optional.Int{IsSet: true, Value: topLogprobs} 438 | return b 439 | } 440 | 441 | // WithResponseFormat sets the response format for the chat completion request. 442 | // 443 | // Forces the model to produce specific output format. 444 | // 445 | // Setting to { "type": "json_object" } enables JSON mode, which guarantees the message the model 446 | // generates is valid JSON. 447 | // 448 | // Setting to { "type": "json_schema", "json_schema": { "name": "...", "schema": {...} } } enables 449 | // the JSON Schema mode, which guarantees the message the model generates is valid JSON and matches 450 | // the provided schema. 451 | // 452 | // Note: when using JSON mode, you should also instruct the model to produce JSON 453 | // yourself via a system or user message. 454 | // 455 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#response-format 456 | // - More info (read all): https://platform.openai.com/docs/guides/structured-outputs 457 | // - Example: https://platform.openai.com/docs/guides/structured-outputs?lang=curl&format=without-parse&api-mode=chat#how-to-use 458 | func (b *chatCompletionBuilder) WithResponseFormat(responseFormat map[string]any) *chatCompletionBuilder { 459 | b.responseFormat = optional.MapStringAny{IsSet: true, Value: responseFormat} 460 | return b 461 | } 462 | 463 | // WithStructuredOutputs sets whether the model can return structured outputs. 464 | // 465 | // If the model can return structured outputs using response_format json_schema. 466 | // 467 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#structured-outputs 468 | func (b *chatCompletionBuilder) WithStructuredOutputs(structuredOutputs bool) *chatCompletionBuilder { 469 | b.structuredOutputs = optional.Bool{IsSet: true, Value: structuredOutputs} 470 | return b 471 | } 472 | 473 | // WithStop Stop generation immediately if the model encounter any token specified in the stop array. 474 | // 475 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#stop 476 | func (b *chatCompletionBuilder) WithStop(stop []string) *chatCompletionBuilder { 477 | b.stop = stop 478 | return b 479 | } 480 | 481 | // WithTool adds a tool to the chat completion request so the model can return a tool call. 482 | // 483 | // If your tool requires parameters, read the [ChatCompletionTool] type documentation 484 | // for more information on how to define the parameters using JSON Schema. 485 | // 486 | // - Models supporting tool calling: https://openrouter.ai/models?supported_parameters=tools 487 | // - JSON Schema reference: https://json-schema.org/understanding-json-schema/reference 488 | // - Tool calling example: https://platform.openai.com/docs/guides/function-calling 489 | func (b *chatCompletionBuilder) WithTool(tool ChatCompletionTool) *chatCompletionBuilder { 490 | b.tools = append(b.tools, chatCompletionToolFunction{Type: "function", Function: tool}) 491 | return b 492 | } 493 | 494 | // WithToolChoice controls which (if any) tool is called by the model. 495 | // 496 | // - none: The model will not call any tool and instead generates a message. 497 | // - auto: The model can pick between generating a message or calling one or more tools. 498 | // - required: The model must call one or more tools. 499 | // 500 | // If you want to force the model to call a specific tool, set the toolChoice parameter 501 | // to the name of the tool you want to call and this will send the tool in the correct 502 | // format to the model. 503 | // 504 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#tool-choice 505 | func (b *chatCompletionBuilder) WithToolChoice(toolChoice string) *chatCompletionBuilder { 506 | b.toolChoice = optional.String{IsSet: true, Value: toolChoice} 507 | return b 508 | } 509 | 510 | // WithMaxPrice sets the maximum price accepted for the chat completion request for both prompt and completion tokens. 511 | // 512 | // For example, the value (1, 2) will route to any provider with a price of <= $1/m prompt tokens and <= $2/m completion tokens. 513 | // 514 | // - Docs: https://openrouter.ai/docs/api-reference/parameters#max-price 515 | func (b *chatCompletionBuilder) WithMaxPrice(maxPromptPrice float64, maxCompletionPrice float64) *chatCompletionBuilder { 516 | b.maxPromptPrice = optional.Float64{IsSet: true, Value: maxPromptPrice} 517 | b.maxCompletionPrice = optional.Float64{IsSet: true, Value: maxCompletionPrice} 518 | return b 519 | } 520 | 521 | // errorResponse is a struct that represents an error response when there is an error 522 | // in the response from the OpenRouter API. 523 | // 524 | // - Docs: https://openrouter.ai/docs/api-reference/errors 525 | type errorResponse struct { 526 | Error struct { 527 | Code int `json:"code"` 528 | Message string `json:"message"` 529 | Metadata map[string]any `json:"metadata"` 530 | } `json:"error"` 531 | } 532 | 533 | // Execute the chat completion request with the configured parameters. 534 | // 535 | // Returns: 536 | // 537 | // - The chat completion builder with the new assistant message added. 538 | // - The response from the OpenRouter API. 539 | // - An error if the request fails. 540 | // 541 | // IMPORTANT: The first return value (the builder) now includes the new assistant message content 542 | // returned by the OpenRouter API, allowing you to continue the conversation seamlessly without 543 | // manually adding the assistant's response. 544 | // 545 | // Example: 546 | // 547 | // completion := client. 548 | // NewChatCompletion(). 549 | // WithModel("..."). 550 | // WithSystemMessage("You are a helpful assistant expert in geography."). 551 | // WithUserMessage("What is the capital of France?") 552 | // 553 | // completion, resp, err := completion.Execute() 554 | // if err != nil { 555 | // // handle error 556 | // } 557 | // 558 | // // Use the response, then you can continue the conversation with the assistant 559 | // fmt.Println("Response: ", resp.Choices[0].Message.Content) 560 | // 561 | // // Use the same builder for another request 562 | // completion = completion.WithUserMessage("Thank you!! Now, what is the capital of Germany?") 563 | // _, resp, err = completion.Execute() 564 | // if err != nil { 565 | // // handle error 566 | // } 567 | // 568 | // fmt.Println("Response: ", resp.Choices[0].Message.Content) 569 | func (b *chatCompletionBuilder) Execute() (*chatCompletionBuilder, ChatCompletionResponse, error) { 570 | if b.executing { 571 | return b, ChatCompletionResponse{}, ErrAlreadyExecuting 572 | } 573 | 574 | b.mu.Lock() 575 | b.executing = true 576 | b.mu.Unlock() 577 | 578 | defer func() { 579 | b.mu.Lock() 580 | b.executing = false 581 | b.mu.Unlock() 582 | }() 583 | 584 | if len(b.messages) == 0 { 585 | return b, ChatCompletionResponse{}, ErrMessagesRequired 586 | } 587 | 588 | requestBodyMap := map[string]any{} 589 | if len(b.messages) > 0 { 590 | requestBodyMap["messages"] = b.messages 591 | } 592 | if b.model.IsSet { 593 | requestBodyMap["model"] = b.model.Value 594 | } 595 | if len(b.fallbackModels) > 0 { 596 | requestBodyMap["models"] = b.fallbackModels 597 | } 598 | if b.temperature.IsSet { 599 | requestBodyMap["temperature"] = b.temperature.Value 600 | } 601 | if b.topP.IsSet { 602 | requestBodyMap["top_p"] = b.topP.Value 603 | } 604 | if b.topK.IsSet { 605 | requestBodyMap["top_k"] = b.topK.Value 606 | } 607 | if b.frequencyPenalty.IsSet { 608 | requestBodyMap["frequency_penalty"] = b.frequencyPenalty.Value 609 | } 610 | if b.presencePenalty.IsSet { 611 | requestBodyMap["presence_penalty"] = b.presencePenalty.Value 612 | } 613 | if b.repetitionPenalty.IsSet { 614 | requestBodyMap["repetition_penalty"] = b.repetitionPenalty.Value 615 | } 616 | if b.minP.IsSet { 617 | requestBodyMap["min_p"] = b.minP.Value 618 | } 619 | if b.topA.IsSet { 620 | requestBodyMap["top_a"] = b.topA.Value 621 | } 622 | if b.seed.IsSet { 623 | requestBodyMap["seed"] = b.seed.Value 624 | } 625 | if b.maxTokens.IsSet { 626 | requestBodyMap["max_tokens"] = b.maxTokens.Value 627 | } 628 | if b.logitBias.IsSet { 629 | requestBodyMap["logit_bias"] = b.logitBias.Value 630 | } 631 | if b.logprobs.IsSet { 632 | requestBodyMap["logprobs"] = b.logprobs.Value 633 | } 634 | if b.topLogprobs.IsSet { 635 | requestBodyMap["top_logprobs"] = b.topLogprobs.Value 636 | } 637 | if b.responseFormat.IsSet { 638 | requestBodyMap["response_format"] = b.responseFormat.Value 639 | } 640 | if b.structuredOutputs.IsSet { 641 | requestBodyMap["structured_outputs"] = b.structuredOutputs.Value 642 | } 643 | if len(b.stop) > 0 { 644 | requestBodyMap["stop"] = b.stop 645 | } 646 | if len(b.tools) > 0 { 647 | requestBodyMap["tools"] = b.tools 648 | } 649 | if b.toolChoice.IsSet { 650 | if slices.Contains([]string{"none", "auto", "required"}, b.toolChoice.Value) { 651 | requestBodyMap["tool_choice"] = b.toolChoice.Value 652 | } else { 653 | requestBodyMap["tool_choice"] = map[string]any{ 654 | "type": "function", 655 | "function": map[string]string{ 656 | "name": b.toolChoice.Value, 657 | }, 658 | } 659 | } 660 | } 661 | if b.maxPromptPrice.IsSet && b.maxCompletionPrice.IsSet { 662 | requestBodyMap["max_price"] = map[string]float64{ 663 | "prompt": b.maxPromptPrice.Value, 664 | "completion": b.maxCompletionPrice.Value, 665 | } 666 | } 667 | 668 | if b.debug { 669 | fmt.Println() 670 | fmt.Println("---------------------------") 671 | fmt.Println("-- Request to OpenRouter --") 672 | fmt.Println("---------------------------") 673 | debug.PrintAsJSON(requestBodyMap) 674 | fmt.Println() 675 | } 676 | 677 | requestBodyBytes, err := json.Marshal(requestBodyMap) 678 | if err != nil { 679 | return b, ChatCompletionResponse{}, fmt.Errorf("failed to marshal request body: %w", err) 680 | } 681 | 682 | req, err := b.client.newRequest(b.ctx, http.MethodPost, "/chat/completions", requestBodyBytes) 683 | if err != nil { 684 | return b, ChatCompletionResponse{}, fmt.Errorf("failed to create request: %w", err) 685 | } 686 | 687 | resp, err := b.client.httpClient.Do(req) 688 | if err != nil { 689 | return b, ChatCompletionResponse{}, fmt.Errorf("failed to send request: %w", err) 690 | } 691 | defer resp.Body.Close() 692 | 693 | bodyBytes, err := io.ReadAll(resp.Body) 694 | if err != nil { 695 | return b, ChatCompletionResponse{}, fmt.Errorf("failed to read response body: %w", err) 696 | } 697 | 698 | var tempResp map[string]any 699 | if err := json.Unmarshal(bodyBytes, &tempResp); err != nil { 700 | return b, ChatCompletionResponse{}, fmt.Errorf("failed to decode response: %w", err) 701 | } 702 | 703 | if b.debug { 704 | fmt.Println() 705 | fmt.Println("------------------------------") 706 | fmt.Println("-- Response from OpenRouter --") 707 | fmt.Println("------------------------------") 708 | fmt.Printf("Status code: %d\n", resp.StatusCode) 709 | debug.PrintAsJSON(tempResp) 710 | fmt.Println() 711 | } 712 | 713 | if tempResp["error"] != nil { 714 | var errorResponse errorResponse 715 | if err := json.Unmarshal(bodyBytes, &errorResponse); err != nil { 716 | return b, ChatCompletionResponse{}, fmt.Errorf("failed to decode error response: %w", err) 717 | } 718 | return b, ChatCompletionResponse{}, fmt.Errorf("request failed with status code %d: %s", resp.StatusCode, errorResponse.Error.Message) 719 | } 720 | 721 | var response ChatCompletionResponse 722 | if err := json.Unmarshal(bodyBytes, &response); err != nil { 723 | return b, ChatCompletionResponse{}, fmt.Errorf("failed to decode response: %w", err) 724 | } 725 | 726 | // Add all the response messages to the builder so we can continue the conversation 727 | if len(response.Choices) > 0 { 728 | for _, choice := range response.Choices { 729 | b.WithMessage(choice.Message) 730 | } 731 | } 732 | 733 | return b, response, nil 734 | } 735 | --------------------------------------------------------------------------------