diff --git a/backend/api/model/app/developer_api/developer_api.go b/backend/api/model/app/developer_api/developer_api.go index 48a9a57ad6..c2cca4937b 100644 --- a/backend/api/model/app/developer_api/developer_api.go +++ b/backend/api/model/app/developer_api/developer_api.go @@ -935,7 +935,9 @@ const ( // name: Llama ModelClass_Llama ModelClass = 20 ModelClass_StepFun ModelClass = 23 - ModelClass_Other ModelClass = 999 + // name: OrcaRouter (OpenAI-compatible meta-router) + ModelClass_OrcaRouter ModelClass = 24 + ModelClass_Other ModelClass = 999 ) func (p ModelClass) String() string { @@ -980,6 +982,8 @@ func (p ModelClass) String() string { return "Llama" case ModelClass_StepFun: return "StepFun" + case ModelClass_OrcaRouter: + return "OrcaRouter" case ModelClass_Other: return "Other" } @@ -1028,6 +1032,8 @@ func ModelClassFromString(s string) (ModelClass, error) { return ModelClass_Llama, nil case "StepFun": return ModelClass_StepFun, nil + case "OrcaRouter": + return ModelClass_OrcaRouter, nil case "Other": return ModelClass_Other, nil } diff --git a/backend/bizpkg/config/modelmgr/deprecate_model_get.go b/backend/bizpkg/config/modelmgr/deprecate_model_get.go index 2b254ebec3..6227cc1ac4 100644 --- a/backend/bizpkg/config/modelmgr/deprecate_model_get.go +++ b/backend/bizpkg/config/modelmgr/deprecate_model_get.go @@ -204,6 +204,8 @@ func strProtocolToModelClass(protocol Protocol) developer_api.ModelClass { modelClass = developer_api.ModelClass_Llama case ProtocolQwen: modelClass = developer_api.ModelClass_QWen + case ProtocolOrcaRouter: + modelClass = developer_api.ModelClass_OrcaRouter default: modelClass = developer_api.ModelClass_SEED } @@ -348,6 +350,9 @@ const ( ProtocolArk Protocol = "ark" ProtocolOllama Protocol = "ollama" ProtocolQwen Protocol = "qwen" + // ProtocolOrcaRouter is an OpenAI-compatible meta-router; it reuses the + // OpenAI wire protocol with a dedicated builder that injects attribution headers. + ProtocolOrcaRouter Protocol = "orcarouter" ) type MultilingualText struct { diff --git a/backend/bizpkg/config/modelmgr/mode_provider.go b/backend/bizpkg/config/modelmgr/mode_provider.go index e3bdb3a79c..bbd390940d 100644 --- a/backend/bizpkg/config/modelmgr/mode_provider.go +++ b/backend/bizpkg/config/modelmgr/mode_provider.go @@ -107,6 +107,18 @@ func getModelProviderList() []*config.ModelProvider { }, ModelClass: developer_api.ModelClass_QWen, }, + { + Name: &config.I18nText{ + ZhCn: "OrcaRouter", + EnUs: "OrcaRouter", + }, + IconURI: "default_icon/orcarouter.png", + Description: &config.I18nText{ + ZhCn: "OrcaRouter 是 OpenAI 兼容的智能路由聚合网关,一个 API key 即可访问多家上游模型并按策略自动路由", + EnUs: "OrcaRouter is an OpenAI-compatible adaptive routing gateway: one API key to reach many upstream models with automatic, policy-based routing", + }, + ModelClass: developer_api.ModelClass_OrcaRouter, + }, } } diff --git a/backend/bizpkg/llm/modelbuilder/model_builder.go b/backend/bizpkg/llm/modelbuilder/model_builder.go index 7b74ff41a0..18367ed028 100644 --- a/backend/bizpkg/llm/modelbuilder/model_builder.go +++ b/backend/bizpkg/llm/modelbuilder/model_builder.go @@ -40,13 +40,14 @@ type Service interface { } var modelClass2NewModelBuilder = map[developer_api.ModelClass]func(*config.Model) Service{ - developer_api.ModelClass_SEED: newArkModelBuilder, - developer_api.ModelClass_GPT: newOpenaiModelBuilder, - developer_api.ModelClass_Claude: newClaudeModelBuilder, - developer_api.ModelClass_DeekSeek: newDeepseekModelBuilder, - developer_api.ModelClass_Gemini: newGeminiModelBuilder, - developer_api.ModelClass_Llama: newOllamaModelBuilder, - developer_api.ModelClass_QWen: newQwenModelBuilder, + developer_api.ModelClass_SEED: newArkModelBuilder, + developer_api.ModelClass_GPT: newOpenaiModelBuilder, + developer_api.ModelClass_Claude: newClaudeModelBuilder, + developer_api.ModelClass_DeekSeek: newDeepseekModelBuilder, + developer_api.ModelClass_Gemini: newGeminiModelBuilder, + developer_api.ModelClass_Llama: newOllamaModelBuilder, + developer_api.ModelClass_QWen: newQwenModelBuilder, + developer_api.ModelClass_OrcaRouter: newOrcaRouterModelBuilder, } func NewModelBuilder(modelClass developer_api.ModelClass, cfg *config.Model) (Service, error) { diff --git a/backend/bizpkg/llm/modelbuilder/orcarouter.go b/backend/bizpkg/llm/modelbuilder/orcarouter.go new file mode 100644 index 0000000000..780cae8bca --- /dev/null +++ b/backend/bizpkg/llm/modelbuilder/orcarouter.go @@ -0,0 +1,156 @@ +/* + * Copyright 2025 coze-dev 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 modelbuilder + +import ( + "context" + "net/http" + "strings" + + "github.com/cloudwego/eino-ext/components/model/openai" + + "github.com/coze-dev/coze-studio/backend/api/model/admin/config" + "github.com/coze-dev/coze-studio/backend/api/model/app/bot_common" + "github.com/coze-dev/coze-studio/backend/pkg/lang/ptr" +) + +// OrcaRouter is an OpenAI-compatible meta-router. It speaks the OpenAI wire +// protocol, so the request/response handling is identical to the OpenAI +// builder; the only OrcaRouter-specific behavior is the attribution headers +// added to every request, which let the OrcaRouter backend attribute traffic +// to Coze Studio. These headers are optional for OpenAI-compatible gateways +// and are ignored by upstreams that do not understand them. +const ( + orcaRouterReferer = "https://github.com/coze-dev/coze-studio" + orcaRouterTitle = "Coze Studio" + // orcaRouterDefaultBaseURL is the canonical OrcaRouter API base; used when + // the connection config does not supply one, so that the orcarouter + // protocol cannot accidentally fall back to the upstream openai default. + orcaRouterDefaultBaseURL = "https://api.orcarouter.ai/v1" +) + +type orcaRouterModelBuilder struct { + cfg *config.Model +} + +func newOrcaRouterModelBuilder(cfg *config.Model) Service { + return &orcaRouterModelBuilder{ + cfg: cfg, + } +} + +// attributionRoundTripper adds OrcaRouter attribution headers to every request +// without overriding any header that the inner transport already set. +type attributionRoundTripper struct { + next http.RoundTripper +} + +func (t *attributionRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + // Clone to avoid mutating the caller's request (http.RoundTripper contract). + r := req.Clone(req.Context()) + if r.Header.Get("HTTP-Referer") == "" { + r.Header.Set("HTTP-Referer", orcaRouterReferer) + } + if r.Header.Get("X-Title") == "" { + r.Header.Set("X-Title", orcaRouterTitle) + } + return t.next.RoundTrip(r) +} + +// suppressesSamplingParams reports whether sampling parameters (temperature, +// top_p, presence/frequency penalty) must be withheld for the given model. +// +// Two cases require this: +// - The adaptive router "orcarouter/auto": the upstream model is chosen per +// request and is unknowable client-side, so any sampling param risks being +// rejected by whatever model the router lands on (reasoning models reject +// temperature/top_k, grok rejects penalties, etc.). +// - A model that is itself a reasoning model: these reject temperature +// outright (HTTP 400 "temperature is deprecated for this model"). +// +// This is a client-side guard. The general, durable fix belongs in the +// OrcaRouter backend (normalize/strip params per chosen upstream, like +// OpenRouter does); once that ships, this guard can be removed. The reasoning +// patterns mirror the override list maintained in the OrcaRouter integration +// notes (§16) and are matched loosely so dated/sub-variants are covered. +func suppressesSamplingParams(model string) bool { + m := strings.ToLower(strings.TrimSpace(model)) + switch { + case m == "auto" || strings.HasSuffix(m, "/auto"): + return true // adaptive router — upstream unknown at request time + case strings.Contains(m, "claude-opus-4"): // Anthropic reasoning flagships (4.x) + return true + case strings.Contains(m, "gpt-5"): // OpenAI gpt-5 family (incl. mini/nano/pro) + return true + case strings.Contains(m, "deepseek-reasoner") || strings.Contains(m, "deepseek-r1"): + return true + default: + return false + } +} + +// applyNonSamplingParams sets only the parameters that every upstream accepts +// (max tokens, response format). It deliberately omits temperature / top_p / +// penalties for adaptive or reasoning models — see suppressesSamplingParams. +func applyNonSamplingParams(conf *openai.ChatModelConfig, params *LLMParams) { + if params == nil { + return + } + if params.MaxTokens != 0 { + conf.MaxCompletionTokens = ptr.Of(params.MaxTokens) + } + if params.ResponseFormat == bot_common.ModelResponseFormat_JSON { + conf.ResponseFormat = &openai.ChatCompletionResponseFormat{ + Type: openai.ChatCompletionResponseFormatTypeJSONObject, + } + } else { + conf.ResponseFormat = &openai.ChatCompletionResponseFormat{ + Type: openai.ChatCompletionResponseFormatTypeText, + } + } +} + +func (o *orcaRouterModelBuilder) Build(ctx context.Context, params *LLMParams) (ToolCallingChatModel, error) { + base := o.cfg.Connection.BaseConnInfo + + // Reuse the OpenAI builder's config construction so request handling stays + // in lockstep with the openai protocol; we only layer attribution on top. + ob := &openaiModelBuilder{cfg: o.cfg} + conf := ob.getDefaultConfig() + conf.APIKey = base.APIKey + conf.Model = base.Model + + if base.BaseURL != "" { + conf.BaseURL = base.BaseURL + } else { + conf.BaseURL = orcaRouterDefaultBaseURL + } + + // Pinned models that accept sampling params get the full openai treatment; + // the adaptive router and reasoning models get only the safe params. + if suppressesSamplingParams(base.Model) { + applyNonSamplingParams(conf, params) + } else { + ob.applyParamsToOpenaiConfig(conf, params) + } + + conf.HTTPClient = &http.Client{ + Transport: &attributionRoundTripper{next: http.DefaultTransport}, + } + + return openai.NewChatModel(ctx, conf) +} diff --git a/backend/bizpkg/llm/modelbuilder/orcarouter_test.go b/backend/bizpkg/llm/modelbuilder/orcarouter_test.go new file mode 100644 index 0000000000..c3ea18810e --- /dev/null +++ b/backend/bizpkg/llm/modelbuilder/orcarouter_test.go @@ -0,0 +1,186 @@ +/* + * Copyright 2025 coze-dev 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 modelbuilder + +import ( + "io" + "net/http" + "strings" + "testing" + + "github.com/cloudwego/eino-ext/components/model/openai" + + "github.com/coze-dev/coze-studio/backend/api/model/admin/config" + "github.com/coze-dev/coze-studio/backend/api/model/app/developer_api" +) + +// captureRoundTripper records the request it receives and returns a dummy 200. +type captureRoundTripper struct { + captured *http.Request +} + +func (c *captureRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + c.captured = req + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("{}")), + Header: make(http.Header), + Request: req, + }, nil +} + +func newTestRequest(t *testing.T) *http.Request { + t.Helper() + req, err := http.NewRequest(http.MethodPost, "https://api.orcarouter.ai/v1/chat/completions", strings.NewReader(`{}`)) + if err != nil { + t.Fatalf("new request: %v", err) + } + return req +} + +func TestAttributionRoundTripper_InjectsHeadersWhenAbsent(t *testing.T) { + cap := &captureRoundTripper{} + rt := &attributionRoundTripper{next: cap} + + if _, err := rt.RoundTrip(newTestRequest(t)); err != nil { + t.Fatalf("round trip: %v", err) + } + + if got := cap.captured.Header.Get("HTTP-Referer"); got != orcaRouterReferer { + t.Errorf("HTTP-Referer = %q, want %q", got, orcaRouterReferer) + } + if got := cap.captured.Header.Get("X-Title"); got != orcaRouterTitle { + t.Errorf("X-Title = %q, want %q", got, orcaRouterTitle) + } +} + +func TestAttributionRoundTripper_DoesNotOverrideExisting(t *testing.T) { + cap := &captureRoundTripper{} + rt := &attributionRoundTripper{next: cap} + + req := newTestRequest(t) + req.Header.Set("HTTP-Referer", "https://custom.example/") + req.Header.Set("X-Title", "Custom Client") + + if _, err := rt.RoundTrip(req); err != nil { + t.Fatalf("round trip: %v", err) + } + + if got := cap.captured.Header.Get("HTTP-Referer"); got != "https://custom.example/" { + t.Errorf("HTTP-Referer overridden: got %q", got) + } + if got := cap.captured.Header.Get("X-Title"); got != "Custom Client" { + t.Errorf("X-Title overridden: got %q", got) + } +} + +func TestAttributionRoundTripper_DoesNotMutateOriginalRequest(t *testing.T) { + cap := &captureRoundTripper{} + rt := &attributionRoundTripper{next: cap} + + req := newTestRequest(t) + if _, err := rt.RoundTrip(req); err != nil { + t.Fatalf("round trip: %v", err) + } + + // The caller's request must be left untouched; headers go on the clone. + if req.Header.Get("HTTP-Referer") != "" || req.Header.Get("X-Title") != "" { + t.Errorf("original request was mutated: %v", req.Header) + } +} + +func TestSuppressesSamplingParams(t *testing.T) { + cases := []struct { + model string + want bool + }{ + // adaptive router — upstream unknown, must suppress + {"orcarouter/auto", true}, + {"auto", true}, + // reasoning models that reject temperature + {"anthropic/claude-opus-4.7", true}, + {"anthropic/claude-opus-4.6", true}, + {"openai/gpt-5", true}, + {"openai/gpt-5.5-pro", true}, + {"deepseek/deepseek-reasoner", true}, + // models that accept sampling params — must NOT suppress + {"openai/gpt-4o", false}, + {"openai/gpt-4o-mini", false}, + {"anthropic/claude-sonnet-4.6", false}, + {"google/gemini-3-flash-preview", false}, + {"", false}, + } + for _, c := range cases { + if got := suppressesSamplingParams(c.model); got != c.want { + t.Errorf("suppressesSamplingParams(%q) = %v, want %v", c.model, got, c.want) + } + } +} + +func TestApplyNonSamplingParams_OmitsTemperature(t *testing.T) { + conf := &openai.ChatModelConfig{} + temp := float32(0.7) + topP := float32(0.9) + applyNonSamplingParams(conf, &LLMParams{ + Temperature: &temp, + TopP: &topP, + FrequencyPenalty: 0.5, + PresencePenalty: 0.5, + MaxTokens: 2048, + }) + if conf.Temperature != nil { + t.Errorf("temperature should be omitted, got %v", *conf.Temperature) + } + if conf.TopP != nil { + t.Errorf("top_p should be omitted, got %v", *conf.TopP) + } + if conf.FrequencyPenalty != nil { + t.Errorf("frequency_penalty should be omitted, got %v", *conf.FrequencyPenalty) + } + if conf.PresencePenalty != nil { + t.Errorf("presence_penalty should be omitted, got %v", *conf.PresencePenalty) + } + // non-sampling params are still applied + if conf.MaxCompletionTokens == nil || *conf.MaxCompletionTokens != 2048 { + t.Errorf("max_completion_tokens should be 2048, got %v", conf.MaxCompletionTokens) + } +} + +func TestOrcaRouterBuilder_RegisteredAndWiresBaseConn(t *testing.T) { + // OrcaRouter must be a supported protocol via the dispatch map. + if !SupportProtocol(developer_api.ModelClass_OrcaRouter) { + t.Fatal("ModelClass_OrcaRouter not registered in modelClass2NewModelBuilder") + } + + cfg := &config.Model{ + Connection: &config.Connection{ + BaseConnInfo: &config.BaseConnectionInfo{ + BaseURL: "https://api.orcarouter.ai/v1", + APIKey: "sk-orca-test", + Model: "orcarouter/auto", + }, + }, + } + + svc, err := NewModelBuilder(developer_api.ModelClass_OrcaRouter, cfg) + if err != nil { + t.Fatalf("NewModelBuilder: %v", err) + } + if _, ok := svc.(*orcaRouterModelBuilder); !ok { + t.Fatalf("expected *orcaRouterModelBuilder, got %T", svc) + } +} diff --git a/backend/conf/model/model_meta.json b/backend/conf/model/model_meta.json index 7d83301a72..f79d886f17 100644 --- a/backend/conf/model/model_meta.json +++ b/backend/conf/model/model_meta.json @@ -5496,6 +5496,442 @@ } ] } + }, + "OrcaRouter": { + "default": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 128000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "orcarouter/auto": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 128000 + }, + "capability": { + "cot_display": true, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": true + }, + "parameters": [ + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "openai/gpt-5": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 400000 + }, + "capability": { + "cot_display": true, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": true + }, + "parameters": [ + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "google/gemini-3-flash-preview": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 1048576 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "anthropic/claude-opus-4.7": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 200000 + }, + "capability": { + "cot_display": true, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": true + }, + "parameters": [ + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "grok/grok-4.3": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 256000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "deepseek/deepseek-v4-pro": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 128000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "minimax/minimax-m2.7": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 1000000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "qwen/qwen3.6-flash": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 1000000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + } } } } \ No newline at end of file diff --git a/backend/conf/model/template/model_template_orcarouter.yaml b/backend/conf/model/template/model_template_orcarouter.yaml new file mode 100644 index 0000000000..065187f0f0 --- /dev/null +++ b/backend/conf/model/template/model_template_orcarouter.yaml @@ -0,0 +1,160 @@ +id: 72010 +name: OrcaRouter Auto +icon_uri: default_icon/orcarouter.png +icon_url: "" +description: + zh: OrcaRouter 是 OpenAI 兼容的智能路由聚合网关。orcarouter/auto 会按策略(成本/质量/延迟/自适应)自动为每个请求挑选上游模型;一个 API key 即可访问全部上游。完整模型清单见 https://www.orcarouter.ai/models 。 + en: OrcaRouter is an OpenAI-compatible adaptive routing gateway. The orcarouter/auto router automatically picks an upstream model per request by policy (cost/quality/latency/adaptive); one API key reaches every upstream. See https://www.orcarouter.ai/models for the full catalog. +default_parameters: + - name: temperature + label: + zh: 生成随机性 + en: Temperature + desc: + zh: '- **temperature**: 调高温度会使得模型的输出更多样性和创新性,反之,降低温度会使输出内容更加遵循指令要求但减少多样性。建议不要与“Top p”同时调整。' + en: '**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.' + type: float + min: "0" + max: "1" + default_val: + balance: "0.8" + creative: "1" + default_val: "1.0" + precise: "0.3" + precision: 1 + options: [] + style: + widget: slider + label: + zh: 生成多样性 + en: Generation diversity + - name: max_tokens + label: + zh: 最大回复长度 + en: Response max length + desc: + zh: 控制模型输出的Tokens 长度上限。通常 100 Tokens 约等于 150 个中文汉字。 + en: You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters. + type: int + min: "1" + max: "4096" + default_val: + default_val: "4096" + options: [] + style: + widget: slider + label: + zh: 输入及输出设置 + en: Input and output settings + - name: top_p + label: + zh: Top P + en: Top P + desc: + zh: '- **Top p 为累计概率**: 模型在生成输出时会从概率最高的词汇开始选择,直到这些词汇的总概率累积达到Top p 值。这样可以限制模型只选择这些高概率的词汇,从而控制输出内容的多样性。建议不要与“生成随机性”同时调整。' + en: '**Top P**:\n\n- An alternative to sampling with temperature, where only tokens within the top p probability mass are considered. For example, 0.1 means only the top 10% probability mass tokens are considered.\n- We recommend altering this or temperature, but not both.' + type: float + min: "0" + max: "1" + default_val: + default_val: "0.7" + precision: 2 + options: [] + style: + widget: slider + label: + zh: 生成多样性 + en: Generation diversity + - name: frequency_penalty + label: + zh: 重复语句惩罚 + en: Frequency penalty + desc: + zh: '- **frequency penalty**: 当该值为正时,会阻止模型频繁使用相同的词汇和短语,从而增加输出内容的多样性。' + en: '**Frequency Penalty**: When positive, it discourages the model from repeating the same words and phrases, thereby increasing the diversity of the output.' + type: float + min: "-2" + max: "2" + default_val: + default_val: "0" + precision: 2 + options: [] + style: + widget: slider + label: + zh: 生成多样性 + en: Generation diversity + - name: presence_penalty + label: + zh: 重复主题惩罚 + en: Presence penalty + desc: + zh: '- **presence penalty**: 当该值为正时,会阻止模型频繁讨论相同的主题,从而增加输出内容的多样性' + en: '**Presence Penalty**: When positive, it prevents the model from discussing the same topics repeatedly, thereby increasing the diversity of the output.' + type: float + min: "-2" + max: "2" + default_val: + default_val: "0" + precision: 2 + options: [] + style: + widget: slider + label: + zh: 生成多样性 + en: Generation diversity + - name: response_format + label: + zh: 输出格式 + en: Response format + desc: + zh: '- **文本**: 使用普通文本格式回复\n- **Markdown**: 将引导模型使用Markdown格式输出回复\n- **JSON**: 将引导模型使用JSON格式输出' + en: '**Response Format**:\n\n- **Text**: Replies in plain text format\n- **Markdown**: Uses Markdown format for replies\n- **JSON**: Uses JSON format for replies' + type: int + min: "" + max: "" + default_val: + default_val: "0" + options: + - label: Text + value: "0" + - label: Markdown + value: "1" + - label: JSON + value: "2" + style: + widget: radio_buttons + label: + zh: 输入及输出设置 + en: Input and output settings +meta: + protocol: orcarouter + capability: + function_call: true + input_modal: + - text + - image + input_tokens: 128000 + json_mode: false + max_tokens: 128000 + output_modal: + - text + output_tokens: 16384 + prefix_caching: false + reasoning: false + prefill_response: false + conn_config: + base_url: "https://api.orcarouter.ai/v1" + api_key: "" + timeout: 0s + model: "orcarouter/auto" + temperature: 0.7 + frequency_penalty: 0 + presence_penalty: 0 + max_tokens: 4096 + max_completion_tokens: 4096 + top_p: 1 + top_k: 0 + stop: [] + custom: {} + status: 0 diff --git a/docker/volumes/minio/default_icon/orcarouter.png b/docker/volumes/minio/default_icon/orcarouter.png new file mode 100644 index 0000000000..1c9e9cd644 Binary files /dev/null and b/docker/volumes/minio/default_icon/orcarouter.png differ diff --git a/frontend/packages/arch/idl/src/auto-generated/developer_api/namespaces/developer_api.ts b/frontend/packages/arch/idl/src/auto-generated/developer_api/namespaces/developer_api.ts index dcc5523c34..147ce72ce7 100644 --- a/frontend/packages/arch/idl/src/auto-generated/developer_api/namespaces/developer_api.ts +++ b/frontend/packages/arch/idl/src/auto-generated/developer_api/namespaces/developer_api.ts @@ -686,6 +686,8 @@ export enum ModelClass { /** name: Llama */ Llama = 20, StepFun = 23, + /** name: OrcaRouter (OpenAI-compatible meta-router) */ + OrcaRouter = 24, Other = 999, } diff --git a/helm/charts/opencoze/files/conf/model/model_meta.json b/helm/charts/opencoze/files/conf/model/model_meta.json index 9419ed9d50..ddc53107b8 100644 --- a/helm/charts/opencoze/files/conf/model/model_meta.json +++ b/helm/charts/opencoze/files/conf/model/model_meta.json @@ -49,7 +49,7 @@ } } ] - } + } }, "GPT": { "default": { @@ -722,6 +722,442 @@ } ] } + }, + "OrcaRouter": { + "default": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 128000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "orcarouter/auto": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 128000 + }, + "capability": { + "cot_display": true, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": true + }, + "parameters": [ + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "openai/gpt-5": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 400000 + }, + "capability": { + "cot_display": true, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": true + }, + "parameters": [ + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "google/gemini-3-flash-preview": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 1048576 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "anthropic/claude-opus-4.7": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 200000 + }, + "capability": { + "cot_display": true, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": true + }, + "parameters": [ + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "grok/grok-4.3": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 256000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "deepseek/deepseek-v4-pro": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 128000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "minimax/minimax-m2.7": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 1000000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + }, + "qwen/qwen3.6-flash": { + "display_info": { + "output_tokens": 4096, + "max_tokens": 1000000 + }, + "capability": { + "cot_display": false, + "function_call": true, + "image_understanding": false, + "video_understanding": false, + "audio_understanding": false, + "support_multi_modal": false, + "prefill_resp": false, + "reasoning": false + }, + "parameters": [ + { + "name": "temperature", + "label": "Temperature", + "desc": "**Temperature**:\n\n- When you increase this value, the model outputs more diverse and innovative content; when you decrease it, the model outputs less diverse content that strictly follows the given instructions.\n- It is recommended not to adjust this value with \"Top p\" at the same time.", + "type": 1, + "min": "0", + "max": "1", + "precision": 1, + "default_val": { + "default_val": "1", + "creative": "1", + "balance": "0.8", + "precise": "0.3" + }, + "options": [], + "param_class": { + "class_id": 1, + "label": "Generation diversity" + } + }, + { + "name": "max_tokens", + "label": "Response max length", + "desc": "You can specify the maximum length of the tokens output through this value. Typically, 100 tokens are approximately equal to 150 Chinese characters.", + "type": 2, + "min": "1", + "max": "4096", + "precision": 0, + "default_val": { + "default_val": "4096" + }, + "options": [], + "param_class": { + "class_id": 2, + "label": "Input and output settings" + } + } + ] + } } } } \ No newline at end of file diff --git a/idl/app/developer_api.thrift b/idl/app/developer_api.thrift index 8767d2315c..39321ec7e4 100644 --- a/idl/app/developer_api.thrift +++ b/idl/app/developer_api.thrift @@ -538,6 +538,7 @@ enum ModelClass { DeekSeek = 19 // Name: Magic Square Llama = 20 // name: Llama StepFun = 23 + OrcaRouter = 24 // name: OrcaRouter (OpenAI-compatible meta-router) Other = 999 }