From 42127af7cc272f1131bb51ed63a16ccbb70d4117 Mon Sep 17 00:00:00 2001 From: Marc-oss-hub Date: Mon, 17 Aug 2026 20:30:02 +0800 Subject: [PATCH] feat(orcarouter): add OrcaRouter chat model component Add components/model/orcarouter, a new standalone module that wraps the shared eino-ext libs/acl/openai client with OrcaRouter's default endpoint (https://api.orcarouter.ai/v1) and namespaced model ids (anthropic/claude-sonnet-5, anthropic/claude-haiku-4.5). Implements model.ChatModel and model.ToolCallingChatModel: Generate, Stream, tool calling, and reasoning_content passthrough for Anthropic thinking models served by OrcaRouter. Includes unit tests, a chat example, and README.md / README_zh.md. Co-Authored-By: Claude --- components/model/orcarouter/README.md | 167 ++++++++++++ components/model/orcarouter/README_zh.md | 165 ++++++++++++ components/model/orcarouter/chatmodel.go | 223 ++++++++++++++++ components/model/orcarouter/chatmodel_test.go | 242 ++++++++++++++++++ .../model/orcarouter/examples/chat/main.go | 52 ++++ components/model/orcarouter/go.mod | 43 ++++ components/model/orcarouter/go.sum | 134 ++++++++++ 7 files changed, 1026 insertions(+) create mode 100644 components/model/orcarouter/README.md create mode 100644 components/model/orcarouter/README_zh.md create mode 100644 components/model/orcarouter/chatmodel.go create mode 100644 components/model/orcarouter/chatmodel_test.go create mode 100644 components/model/orcarouter/examples/chat/main.go create mode 100644 components/model/orcarouter/go.mod create mode 100644 components/model/orcarouter/go.sum diff --git a/components/model/orcarouter/README.md b/components/model/orcarouter/README.md new file mode 100644 index 000000000..0ed80e84f --- /dev/null +++ b/components/model/orcarouter/README.md @@ -0,0 +1,167 @@ +# OrcaRouter + +An [OrcaRouter](https://www.orcarouter.ai) implementation for [Eino](https://github.com/cloudwego/eino) that implements the `ChatModel` interface. This enables seamless integration with Eino's LLM capabilities for enhanced natural language processing and generation. + +OrcaRouter is an AI gateway that serves OpenAI-compatible (`/v1/chat/completions`), Anthropic-compatible (`/v1/messages`) and embedding endpoints from one base URL, with namespaced model ids such as `anthropic/claude-sonnet-5` and `anthropic/claude-haiku-4.5`. It also runs gateway-level, zero-trust security for AI agents on the same endpoint — screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes. + +## Features + +- Implements `github.com/cloudwego/eino/components/model.Model` +- Easy integration with Eino's model system +- Configurable model parameters +- Support for chat completion +- Support for streaming responses +- Support for tool calling +- Thinking-model `reasoning_content` passthrough for Anthropic models served by OrcaRouter + +## Installation + +```bash +go get github.com/cloudwego/eino-ext/components/model/orcarouter@latest +``` + +## Quick start + +Here's a quick example of how to use the OrcaRouter model: + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/cloudwego/eino/schema" + + "github.com/cloudwego/eino-ext/components/model/orcarouter" +) + +func main() { + ctx := context.Background() + + chatModel, err := orcarouter.NewChatModel(ctx, &orcarouter.Config{ + APIKey: os.Getenv("ORCAROUTER_API_KEY"), + Model: os.Getenv("ORCAROUTER_MODEL"), // e.g. anthropic/claude-sonnet-5 + }) + if err != nil { + log.Fatalf("NewChatModel failed, err=%v", err) + } + + resp, err := chatModel.Generate(ctx, []*schema.Message{ + { + Role: schema.User, + Content: "as a machine, how do you answer user's question?", + }, + }) + if err != nil { + log.Fatalf("Generate failed, err=%v", err) + } + fmt.Printf("output: \n%v", resp) +} +``` + +## Configuration + +The model can be configured using the `orcarouter.Config` struct: + +```go +type Config struct { + APIKey string + // Timeout specifies the maximum duration to wait for API responses. + // If HTTPClient is set, Timeout will not be used. + // Optional. Default: no timeout + Timeout time.Duration `json:"timeout"` + + // HTTPClient specifies the client to send HTTP requests. + // If HTTPClient is set, Timeout will not be used. + // Optional. Default &http.Client{Timeout: Timeout} + HTTPClient *http.Client `json:"http_client"` + + // BaseURL specifies the OrcaRouter endpoint URL. + // Optional. Default: https://api.orcarouter.ai/v1 + BaseURL string `json:"base_url"` + + // Model specifies the ID of the model to use. + // OrcaRouter serves namespaced model ids (e.g. anthropic/claude-sonnet-5, + // anthropic/claude-haiku-4.5) over one OpenAI-compatible endpoint. + // Optional. + Model string `json:"model,omitempty"` + + // MaxTokens represents the maximum number of tokens that can be generated in the chat completion. + // Optional. Default: model's maximum + MaxTokens *int `json:"max_tokens,omitempty"` + + // MaxCompletionTokens represents the total number of tokens in the model's output, + // including both the final output and any tokens generated during the thinking process. + MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"` + + // Temperature specifies what sampling temperature to use. + // Generally recommend altering this or TopP but not both. + // Range: 0.0 to 2.0. Higher values make output more random. + // Optional. Default: 1.0 + Temperature *float32 `json:"temperature,omitempty"` + + // TopP controls diversity via nucleus sampling. + // Generally recommend altering this or Temperature but not both. + // Range: 0.0 to 1.0. Lower values make output more focused. + // Optional. Default: 1.0 + TopP *float32 `json:"top_p,omitempty"` + + // Stop sequences where the API will stop generating further tokens. + // Optional. Example: []string{"\n", "User:"} + Stop []string `json:"stop,omitempty"` + + // PresencePenalty prevents repetition by penalizing tokens based on presence. + // Range: -2.0 to 2.0. Positive values increase likelihood of new topics. + // Optional. Default: 0 + PresencePenalty *float32 `json:"presence_penalty,omitempty"` + + // FrequencyPenalty prevents repetition by penalizing tokens based on frequency. + // Range: -2.0 to 2.0. Positive values decrease likelihood of repetition. + // Optional. Default: 0 + FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` + + // Seed enables deterministic sampling for consistent outputs. + // Optional. Set for reproducible results. + Seed *int `json:"seed,omitempty"` + + // User unique identifier representing end-user. + // Optional. + User *string `json:"user,omitempty"` + + // LogitBias modifies the likelihood of specified tokens appearing in the completion. + // Optional. Map token IDs to bias values from -100 to 100. + LogitBias map[string]int `json:"logit_bias,omitempty"` + + // LogProbs specifies whether to return log probabilities of the output tokens. + LogProbs bool `json:"log_probs"` + + // TopLogProbs is an integer between 0 and 20 specifying the number of most likely tokens + // to return at each token position, each with an associated log probability. + // logprobs must be set to true if this parameter is used. + TopLogProbs int `json:"top_logprobs"` + + // ResponseFormat specifies the format of the model's response. + // Optional. Use for structured outputs. + ResponseFormat *openai.ChatCompletionResponseFormat `json:"response_format,omitempty"` + + // ExtraFields will override any existing fields with the same key. + // Optional. Useful for experimental features not yet officially supported. + ExtraFields map[string]any `json:"extra_fields,omitempty"` +} +``` + +## Examples + +See the following examples for more usage: + +- [Basic Chat Completion](./examples/chat/) + + + +## For More Details + +- [Eino Documentation](https://github.com/cloudwego/eino) +- [OrcaRouter](https://www.orcarouter.ai) diff --git a/components/model/orcarouter/README_zh.md b/components/model/orcarouter/README_zh.md new file mode 100644 index 000000000..6aae322e1 --- /dev/null +++ b/components/model/orcarouter/README_zh.md @@ -0,0 +1,165 @@ +# OrcaRouter + +[Eino](https://github.com/cloudwego/eino) 的 [OrcaRouter](https://www.orcarouter.ai) 实现,实现了 `ChatModel` 接口。这使得与 Eino 的 LLM 功能无缝集成,以增强自然语言处理和生成能力。 + +OrcaRouter 是一个 AI 网关,在同一个 base URL 上提供 OpenAI 兼容(`/v1/chat/completions`)、Anthropic 兼容(`/v1/messages`)以及 Embedding 端点,并使用命名空间模型 ID,例如 `anthropic/claude-sonnet-5` 和 `anthropic/claude-haiku-4.5`。它还在同一端点上运行网关级别的零信任 AI 代理安全防护——以默认拒绝的方式对每个 prompt/response 进行筛查并管理每个工具调用,无需修改任何应用代码。 + +## 功能 + +- 实现 `github.com/cloudwego/eino/components/model.Model` +- 轻松与 Eino 的模型系统集成 +- 可配置的模型参数 +- 支持聊天补全 +- 支持流式响应 +- 支持工具调用 +- 透传 OrcaRouter 提供的 Anthropic 模型思考内容(`reasoning_content`) + +## 安装 + +```bash +go get github.com/cloudwego/eino-ext/components/model/orcarouter@latest +``` + +## 快速开始 + +以下是如何使用 OrcaRouter 模型的快速示例: + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/cloudwego/eino/schema" + + "github.com/cloudwego/eino-ext/components/model/orcarouter" +) + +func main() { + ctx := context.Background() + + chatModel, err := orcarouter.NewChatModel(ctx, &orcarouter.Config{ + APIKey: os.Getenv("ORCAROUTER_API_KEY"), + Model: os.Getenv("ORCAROUTER_MODEL"), // 例如 anthropic/claude-sonnet-5 + }) + if err != nil { + log.Fatalf("NewChatModel failed, err=%v", err) + } + + resp, err := chatModel.Generate(ctx, []*schema.Message{ + { + Role: schema.User, + Content: "as a machine, how do you answer user's question?", + }, + }) + if err != nil { + log.Fatalf("Generate failed, err=%v", err) + } + fmt.Printf("output: \n%v", resp) +} +``` + +## 配置 + +模型可以使用 `orcarouter.Config` 结构体进行配置: + +```go +type Config struct { + APIKey string + // Timeout 指定等待 API 响应的最大时长。 + // 如果设置了 HTTPClient,则不会使用 Timeout。 + // 可选。默认:无超时 + Timeout time.Duration `json:"timeout"` + + // HTTPClient 指定用于发送 HTTP 请求的客户端。 + // 如果设置了 HTTPClient,则不会使用 Timeout。 + // 可选。默认 &http.Client{Timeout: Timeout} + HTTPClient *http.Client `json:"http_client"` + + // BaseURL 指定 OrcaRouter 端点 URL。 + // 可选。默认:https://api.orcarouter.ai/v1 + BaseURL string `json:"base_url"` + + // Model 指定要使用的模型 ID。 + // OrcaRouter 在同一个 OpenAI 兼容端点上提供命名空间模型 ID + // (例如 anthropic/claude-sonnet-5、anthropic/claude-haiku-4.5)。 + // 可选。 + Model string `json:"model,omitempty"` + + // MaxTokens 表示聊天补全中可生成的最大 token 数。 + // 可选。默认:模型的最大值 + MaxTokens *int `json:"max_tokens,omitempty"` + + // MaxCompletionTokens 表示模型输出的 token 总数上限, + // 包括最终输出和思考过程中生成的任何 token。 + MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"` + + // Temperature 指定要使用的采样温度。 + // 通常建议只修改此项或 TopP,不要同时修改。 + // 范围:0.0 到 2.0。较高的值使输出更随机。 + // 可选。默认:1.0 + Temperature *float32 `json:"temperature,omitempty"` + + // TopP 通过核采样控制多样性。 + // 通常建议只修改此项或 Temperature,不要同时修改。 + // 范围:0.0 到 1.0。较低的值使输出更集中。 + // 可选。默认:1.0 + TopP *float32 `json:"top_p,omitempty"` + + // Stop 是 API 停止生成更多 token 的停止序列。 + // 可选。示例:[]string{"\n", "User:"} + Stop []string `json:"stop,omitempty"` + + // PresencePenalty 根据 token 的存在来惩罚重复。 + // 范围:-2.0 到 2.0。正值增加新话题的可能性。 + // 可选。默认:0 + PresencePenalty *float32 `json:"presence_penalty,omitempty"` + + // FrequencyPenalty 根据 token 的频率来惩罚重复。 + // 范围:-2.0 到 2.0。负值降低重复的可能性。 + // 可选。默认:0 + FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` + + // Seed 启用确定性采样以获得一致的输出。 + // 可选。为可复现的结果设置。 + Seed *int `json:"seed,omitempty"` + + // User 代表最终用户的唯一标识符。 + // 可选。 + User *string `json:"user,omitempty"` + + // LogitBias 修改指定 token 出现在补全中的可能性。 + // 可选。将 token ID 映射到 -100 到 100 之间的偏差值。 + LogitBias map[string]int `json:"logit_bias,omitempty"` + + // LogProbs 指定是否返回输出 token 的对数概率。 + LogProbs bool `json:"log_probs"` + + // TopLogProbs 是一个 0 到 20 之间的整数,指定在每个 token 位置返回的最可能的 + // token 数量,每个 token 都带有相关的对数概率。 + // 如果使用此参数,则必须将 logprobs 设置为 true。 + TopLogProbs int `json:"top_logprobs"` + + // ResponseFormat 指定模型响应的格式。 + // 可选。用于结构化输出。 + ResponseFormat *openai.ChatCompletionResponseFormat `json:"response_format,omitempty"` + + // ExtraFields 将覆盖任何具有相同 key 的现有字段。 + // 可选。对于尚未正式支持的实验性功能很有用。 + ExtraFields map[string]any `json:"extra_fields,omitempty"` +} +``` + +## 示例 + +更多用法请参阅以下示例: + +- [基础聊天补全](./examples/chat/) + +## 更多详情 + +- [Eino 文档](https://github.com/cloudwego/eino) +- [OrcaRouter](https://www.orcarouter.ai) diff --git a/components/model/orcarouter/chatmodel.go b/components/model/orcarouter/chatmodel.go new file mode 100644 index 000000000..12816d770 --- /dev/null +++ b/components/model/orcarouter/chatmodel.go @@ -0,0 +1,223 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package orcarouter + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/schema" + + "github.com/cloudwego/eino-ext/libs/acl/openai" +) + +const ( + defaultBaseURL = "https://api.orcarouter.ai/v1" +) + +// Config is the configuration for the OrcaRouter ChatModel. +type Config struct { + // APIKey is your OrcaRouter API key. + // Required. + APIKey string + + // Timeout specifies the maximum duration to wait for API responses. + // If HTTPClient is set, Timeout will not be used. + // Optional. Default: no timeout + Timeout time.Duration `json:"timeout"` + + // HTTPClient specifies the client to send HTTP requests. + // If HTTPClient is set, Timeout will not be used. + // Optional. Default &http.Client{Timeout: Timeout} + HTTPClient *http.Client `json:"http_client"` + + // BaseURL specifies the OrcaRouter endpoint URL. + // Optional. Default: https://api.orcarouter.ai/v1 + BaseURL string `json:"base_url"` + + // Model specifies the ID of the model to use. + // OrcaRouter serves namespaced model ids (e.g. anthropic/claude-sonnet-5, + // anthropic/claude-haiku-4.5) over one OpenAI-compatible endpoint. + // Optional. + Model string `json:"model,omitempty"` + + // MaxTokens represents the maximum number of tokens that can be generated in the chat completion. + // Optional. Default: model's maximum + MaxTokens *int `json:"max_tokens,omitempty"` + + // MaxCompletionTokens represents the total number of tokens in the model's output, + // including both the final output and any tokens generated during the thinking process. + MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"` + + // Temperature specifies what sampling temperature to use. + // Generally recommend altering this or TopP but not both. + // Range: 0.0 to 2.0. Higher values make output more random. + // Optional. Default: 1.0 + Temperature *float32 `json:"temperature,omitempty"` + + // TopP controls diversity via nucleus sampling. + // Generally recommend altering this or Temperature but not both. + // Range: 0.0 to 1.0. Lower values make output more focused. + // Optional. Default: 1.0 + TopP *float32 `json:"top_p,omitempty"` + + // Stop sequences where the API will stop generating further tokens. + // Optional. Example: []string{"\n", "User:"} + Stop []string `json:"stop,omitempty"` + + // PresencePenalty prevents repetition by penalizing tokens based on presence. + // Range: -2.0 to 2.0. Positive values increase likelihood of new topics. + // Optional. Default: 0 + PresencePenalty *float32 `json:"presence_penalty,omitempty"` + + // FrequencyPenalty prevents repetition by penalizing tokens based on frequency. + // Range: -2.0 to 2.0. Positive values decrease likelihood of repetition. + // Optional. Default: 0 + FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` + + // Seed enables deterministic sampling for consistent outputs. + // Optional. Set for reproducible results. + Seed *int `json:"seed,omitempty"` + + // User unique identifier representing end-user. + // Optional. + User *string `json:"user,omitempty"` + + // LogitBias modifies the likelihood of specified tokens appearing in the completion. + // Optional. Map token IDs to bias values from -100 to 100. + LogitBias map[string]int `json:"logit_bias,omitempty"` + + // LogProbs specifies whether to return log probabilities of the output tokens. + LogProbs bool `json:"log_probs"` + + // TopLogProbs is an integer between 0 and 20 specifying the number of most likely tokens + // to return at each token position, each with an associated log probability. + // logprobs must be set to true if this parameter is used. + TopLogProbs int `json:"top_logprobs"` + + // ResponseFormat specifies the format of the model's response. + // Optional. Use for structured outputs. + ResponseFormat *openai.ChatCompletionResponseFormat `json:"response_format,omitempty"` + + // ExtraFields will override any existing fields with the same key. + // Optional. Useful for experimental features not yet officially supported. + ExtraFields map[string]any `json:"extra_fields,omitempty"` +} + +// ChatModel is an OrcaRouter implementation of model.ChatModel. +type ChatModel struct { + cli *openai.Client +} + +var ( + _ model.ChatModel = (*ChatModel)(nil) + _ model.ToolCallingChatModel = (*ChatModel)(nil) +) + +// NewChatModel creates a new OrcaRouter ChatModel. +func NewChatModel(ctx context.Context, config *Config) (*ChatModel, error) { + if config == nil { + return nil, fmt.Errorf("config cannot be nil") + } + + var httpClient *http.Client + if config.HTTPClient != nil { + httpClient = config.HTTPClient + } else { + httpClient = &http.Client{Timeout: config.Timeout} + } + + if config.BaseURL == "" { + config.BaseURL = defaultBaseURL + } + + nConf := &openai.Config{ + BaseURL: config.BaseURL, + APIKey: config.APIKey, + HTTPClient: httpClient, + Model: config.Model, + MaxTokens: config.MaxTokens, + MaxCompletionTokens: config.MaxCompletionTokens, + Temperature: config.Temperature, + TopP: config.TopP, + Stop: config.Stop, + PresencePenalty: config.PresencePenalty, + Seed: config.Seed, + FrequencyPenalty: config.FrequencyPenalty, + LogitBias: config.LogitBias, + LogProbs: config.LogProbs, + TopLogProbs: config.TopLogProbs, + User: config.User, + ResponseFormat: config.ResponseFormat, + ExtraFields: config.ExtraFields, + } + + cli, err := openai.NewClient(ctx, nConf) + if err != nil { + return nil, err + } + + return &ChatModel{cli: cli}, nil +} + +// Generate generates a single chat completion. +func (cm *ChatModel) Generate(ctx context.Context, in []*schema.Message, opts ...model.Option) (outMsg *schema.Message, err error) { + ctx = callbacks.EnsureRunInfo(ctx, cm.GetType(), components.ComponentOfChatModel) + return cm.cli.Generate(ctx, in, opts...) +} + +// Stream streams chat completion responses. +func (cm *ChatModel) Stream(ctx context.Context, in []*schema.Message, opts ...model.Option) (outStream *schema.StreamReader[*schema.Message], err error) { + ctx = callbacks.EnsureRunInfo(ctx, cm.GetType(), components.ComponentOfChatModel) + return cm.cli.Stream(ctx, in, opts...) +} + +// WithTools returns a new ChatModel with the given tools bound. +func (cm *ChatModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) { + cli, err := cm.cli.WithToolsForClient(tools) + if err != nil { + return nil, err + } + return &ChatModel{cli: cli}, nil +} + +// BindTools binds the given tools to this ChatModel. +func (cm *ChatModel) BindTools(tools []*schema.ToolInfo) error { + return cm.cli.BindTools(tools) +} + +// BindForcedTools binds the given tools to this ChatModel and forces the model to call one of them. +func (cm *ChatModel) BindForcedTools(tools []*schema.ToolInfo) error { + return cm.cli.BindForcedTools(tools) +} + +const typ = "OrcaRouter" + +// GetType returns the type of this ChatModel. +func (cm *ChatModel) GetType() string { + return typ +} + +// IsCallbacksEnabled returns whether callbacks are enabled. +func (cm *ChatModel) IsCallbacksEnabled() bool { + return cm.cli.IsCallbacksEnabled() +} diff --git a/components/model/orcarouter/chatmodel_test.go b/components/model/orcarouter/chatmodel_test.go new file mode 100644 index 000000000..f1532a15d --- /dev/null +++ b/components/model/orcarouter/chatmodel_test.go @@ -0,0 +1,242 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package orcarouter + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudwego/eino/schema" +) + +func TestNewChatModelNilConfig(t *testing.T) { + _, err := NewChatModel(context.Background(), nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "config cannot be nil") +} + +func TestNewChatModelDefaultBaseURL(t *testing.T) { + // defaultBaseURL must point at OrcaRouter's OpenAI-compatible endpoint. + assert.Equal(t, "https://api.orcarouter.ai/v1", defaultBaseURL) + + cm, err := NewChatModel(context.Background(), &Config{APIKey: "sk-orca-test", Model: "anthropic/claude-haiku-4.5"}) + require.NoError(t, err) + assert.NotNil(t, cm) + assert.Equal(t, "OrcaRouter", cm.GetType()) +} + +func TestGenerate(t *testing.T) { + var gotPath string + var gotAuth string + var gotBody map[string]any + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + gotAuth = r.Header.Get("Authorization") + b, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(b, &gotBody) + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1677652288, + "model": "anthropic/claude-haiku-4.5", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": "Hello from OrcaRouter!"}, + "finish_reason": "stop" + }], + "usage": {"prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21} + }`)) + })) + defer srv.Close() + + cm, err := NewChatModel(context.Background(), &Config{ + APIKey: "sk-orca-test", + BaseURL: srv.URL, + Model: "anthropic/claude-haiku-4.5", + }) + require.NoError(t, err) + + out, err := cm.Generate(context.Background(), []*schema.Message{ + {Role: schema.User, Content: "hi"}, + }) + require.NoError(t, err) + assert.Equal(t, "Hello from OrcaRouter!", out.Content) + assert.Equal(t, schema.Assistant, out.Role) + assert.Equal(t, "stop", out.ResponseMeta.FinishReason) + assert.Equal(t, 21, out.ResponseMeta.Usage.TotalTokens) + + // Request shape: OpenAI-compatible chat/completions path + bearer auth + model/messages. + assert.Equal(t, "/chat/completions", gotPath) + assert.Equal(t, "Bearer sk-orca-test", gotAuth) + assert.Equal(t, "anthropic/claude-haiku-4.5", gotBody["model"]) + msgs := gotBody["messages"].([]any) + assert.Equal(t, "user", msgs[0].(map[string]any)["role"]) + assert.Equal(t, "hi", msgs[0].(map[string]any)["content"]) +} + +func TestGenerateReasoningContent(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1677652288, + "model": "anthropic/claude-sonnet-5", + "choices": [{ + "index": 0, + "message": {"role": "assistant", "content": "Final answer", "reasoning_content": "Let me think step by step."}, + "finish_reason": "stop" + }], + "usage": {"prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21} + }`)) + })) + defer srv.Close() + + cm, err := NewChatModel(context.Background(), &Config{APIKey: "sk-orca-test", BaseURL: srv.URL, Model: "anthropic/claude-sonnet-5"}) + require.NoError(t, err) + + out, err := cm.Generate(context.Background(), []*schema.Message{{Role: schema.User, Content: "1+1?"}}) + require.NoError(t, err) + assert.Equal(t, "Final answer", out.Content) + // OrcaRouter's OpenAI-compatible endpoint surfaces thinking models' reasoning via reasoning_content. + assert.Equal(t, "Let me think step by step.", out.ReasoningContent) +} + +func TestStream(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte( + "data: {\"id\":\"chatcmpl-test\",\"object\":\"chat.completion.chunk\",\"created\":1677652288,\"model\":\"anthropic/claude-haiku-4.5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hello\"},\"finish_reason\":null}]}\n\n" + + "data: {\"id\":\"chatcmpl-test\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" world\"},\"finish_reason\":null}]}\n\n" + + "data: {\"id\":\"chatcmpl-test\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":9,\"completion_tokens\":2,\"total_tokens\":11}}\n\n" + + "data: [DONE]\n\n", + )) + })) + defer srv.Close() + + cm, err := NewChatModel(context.Background(), &Config{APIKey: "sk-orca-test", BaseURL: srv.URL, Model: "anthropic/claude-haiku-4.5"}) + require.NoError(t, err) + + stream, err := cm.Stream(context.Background(), []*schema.Message{{Role: schema.User, Content: "hi"}}) + require.NoError(t, err) + defer stream.Close() + + var contents []string + for { + msg, err := stream.Recv() + if err != nil { + if err == io.EOF { + break + } + t.Fatalf("stream recv failed: %v", err) + } + if msg.Content != "" { + contents = append(contents, msg.Content) + } + } + + assert.Equal(t, []string{"Hello", " world"}, contents) +} + +func TestWithTools(t *testing.T) { + var gotBody map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(b, &gotBody) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1677652288, + "model": "anthropic/claude-haiku-4.5", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [{ + "id": "call_1", + "type": "function", + "function": {"name": "get_weather", "arguments": "{\"city\":\"Shanghai\"}"} + }] + }, + "finish_reason": "tool_calls" + }], + "usage": {"prompt_tokens": 20, "completion_tokens": 10, "total_tokens": 30} + }`)) + })) + defer srv.Close() + + cm, err := NewChatModel(context.Background(), &Config{APIKey: "sk-orca-test", BaseURL: srv.URL, Model: "anthropic/claude-haiku-4.5"}) + require.NoError(t, err) + + toolCallee, err := cm.WithTools([]*schema.ToolInfo{ + { + Name: "get_weather", + Desc: "Get the weather for a city", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "city": {Type: schema.String, Required: true}, + }), + }, + }) + require.NoError(t, err) + + out, err := toolCallee.Generate(context.Background(), []*schema.Message{{Role: schema.User, Content: "weather in Shanghai?"}}) + require.NoError(t, err) + require.Len(t, out.ToolCalls, 1) + assert.Equal(t, "get_weather", out.ToolCalls[0].Function.Name) + assert.Equal(t, "tool_calls", out.ResponseMeta.FinishReason) + + // tools must be attached to the request body. + tools := gotBody["tools"].([]any) + assert.Equal(t, "get_weather", tools[0].(map[string]any)["function"].(map[string]any)["name"]) +} + +func TestBindForcedTools(t *testing.T) { + cm, err := NewChatModel(context.Background(), &Config{APIKey: "sk-orca-test", BaseURL: "http://127.0.0.1:1", Model: "anthropic/claude-haiku-4.5"}) + require.NoError(t, err) + + err = cm.BindForcedTools([]*schema.ToolInfo{ + { + Name: "get_weather", + Desc: "Get the weather for a city", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "city": {Type: schema.String, Required: true}, + }), + }, + }) + require.NoError(t, err) + + // empty tool list must error + err = cm.BindForcedTools(nil) + require.Error(t, err) +} + +func TestGetType(t *testing.T) { + cm := &ChatModel{} + assert.Equal(t, "OrcaRouter", cm.GetType()) +} diff --git a/components/model/orcarouter/examples/chat/main.go b/components/model/orcarouter/examples/chat/main.go new file mode 100644 index 000000000..8ddc12454 --- /dev/null +++ b/components/model/orcarouter/examples/chat/main.go @@ -0,0 +1,52 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/cloudwego/eino/schema" + + "github.com/cloudwego/eino-ext/components/model/orcarouter" +) + +func main() { + ctx := context.Background() + + chatModel, err := orcarouter.NewChatModel(ctx, &orcarouter.Config{ + APIKey: os.Getenv("ORCAROUTER_API_KEY"), + Model: os.Getenv("ORCAROUTER_MODEL"), // e.g. anthropic/claude-sonnet-5 + BaseURL: os.Getenv("ORCAROUTER_BASE_URL"), + }) + if err != nil { + log.Fatalf("NewChatModel failed, err=%v", err) + } + + resp, err := chatModel.Generate(ctx, []*schema.Message{ + { + Role: schema.User, + Content: "as a machine, how do you answer user's question?", + }, + }) + if err != nil { + log.Fatalf("Generate failed, err=%v", err) + } + fmt.Printf("output: \n%v", resp) +} diff --git a/components/model/orcarouter/go.mod b/components/model/orcarouter/go.mod new file mode 100644 index 000000000..f6f103fbe --- /dev/null +++ b/components/model/orcarouter/go.mod @@ -0,0 +1,43 @@ +module github.com/cloudwego/eino-ext/components/model/orcarouter + +go 1.18 + +require ( + github.com/cloudwego/eino v0.7.13 + github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.1 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eino-contrib/jsonschema v1.0.3 // indirect + github.com/evanphx/json-patch v0.5.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/goph/emperror v0.17.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/meguminnnnnnnnn/go-openai v0.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nikolalohinski/gonja v1.5.3 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/yargevad/filepathx v1.0.0 // indirect + golang.org/x/arch v0.11.0 // indirect + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/sys v0.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/components/model/orcarouter/go.sum b/components/model/orcarouter/go.sum new file mode 100644 index 000000000..130ff90c1 --- /dev/null +++ b/components/model/orcarouter/go.sum @@ -0,0 +1,134 @@ +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/mockey v1.3.0 h1:ONLRdvhqmCfr9rTasUB8ZKCfvbdD2tohOg4u+4Q/ed0= +github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= +github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cloudwego/eino v0.7.13 h1:Ku7hY+83gGJJjf4On3UgqjC57UcA+DXe0tqAZiNDDew= +github.com/cloudwego/eino v0.7.13/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ= +github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 h1:EeVcR1TslRA2IdNW1h/2LaGbPlffwGhQm99jM3zWZiI= +github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17/go.mod h1:Zkcx6DPTR2NfWmtSXbhItswGw6hqUezNPhNcke0pOG8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0= +github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/meguminnnnnnnnn/go-openai v0.1.2 h1:iXombGGjqjBrmE9WaSidUhhi3YQhf42QTHvHLMkgvCA= +github.com/meguminnnnnnnnn/go-openai v0.1.2/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= +golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=