├── .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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |