From 9ac7a9f55e31cc0e2e150a9591769f589f0a94be Mon Sep 17 00:00:00 2001 From: Elams Date: Sun, 16 Aug 2026 23:37:00 +0000 Subject: [PATCH] feat(kagent-demo): add OrcaRouter as a named OpenAI-compatible provider --- examples/kagent-demo/.env.example | 9 ++++ examples/kagent-demo/README.md | 12 +++++ examples/kagent-demo/go.sum | 2 + .../kagent-demo/internal/llmbridge/bridge.go | 9 ++-- .../internal/llmbridge/bridge_test.go | 46 +++++++++++++++++++ .../kagent-demo/internal/llmconfig/config.go | 7 +-- .../internal/llmconfig/config_test.go | 22 +++++++++ 7 files changed, 100 insertions(+), 7 deletions(-) diff --git a/examples/kagent-demo/.env.example b/examples/kagent-demo/.env.example index 16d39973b..dddbb46f8 100644 --- a/examples/kagent-demo/.env.example +++ b/examples/kagent-demo/.env.example @@ -13,3 +13,12 @@ MODEL_TIMEOUT=30s # MODEL_API_KEY=replace-me # MODEL_NAME=claude-compatible # MODEL_TIMEOUT=30s + +# OrcaRouter (https://www.orcarouter.ai) — OpenAI-compatible LLM routing gateway. +# Uses the same Chat Completions protocol as the OpenAI block above. +# Uncomment this block and comment the OpenAI block above to use it. +# MODEL_PROVIDER=orcarouter +# MODEL_BASE_URL=https://api.orcarouter.ai/v1 +# MODEL_API_KEY=sk-orca-xxx +# MODEL_NAME=orcarouter/auto +# MODEL_TIMEOUT=30s diff --git a/examples/kagent-demo/README.md b/examples/kagent-demo/README.md index d73aa517d..c56da6b02 100644 --- a/examples/kagent-demo/README.md +++ b/examples/kagent-demo/README.md @@ -39,6 +39,18 @@ MODEL_NAME=claude-compatible MODEL_TIMEOUT=30s ``` +OrcaRouter example ([OrcaRouter](https://www.orcarouter.ai) is an OpenAI-compatible +LLM routing gateway — same protocol as the OpenAI block above, with gateway-level +zero-trust security for AI agents on the same endpoint): + +```bash +MODEL_PROVIDER=orcarouter +MODEL_BASE_URL=https://api.orcarouter.ai/v1 +MODEL_API_KEY=sk-orca-xxx +MODEL_NAME=orcarouter/auto +MODEL_TIMEOUT=30s +``` + ## Run This project defaults to the China Go proxy for local Make targets and Docker diff --git a/examples/kagent-demo/go.sum b/examples/kagent-demo/go.sum index e2befa736..924c4ffaa 100644 --- a/examples/kagent-demo/go.sum +++ b/examples/kagent-demo/go.sum @@ -89,6 +89,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= diff --git a/examples/kagent-demo/internal/llmbridge/bridge.go b/examples/kagent-demo/internal/llmbridge/bridge.go index 89672fed0..465ca6eba 100644 --- a/examples/kagent-demo/internal/llmbridge/bridge.go +++ b/examples/kagent-demo/internal/llmbridge/bridge.go @@ -18,8 +18,9 @@ import ( ) const ( - ProviderOpenAI = llmconfig.ProviderOpenAI - ProviderAnthropic = llmconfig.ProviderAnthropic + ProviderOpenAI = llmconfig.ProviderOpenAI + ProviderAnthropic = llmconfig.ProviderAnthropic + ProviderOrcaRouter = llmconfig.ProviderOrcaRouter ) type Bridge struct { @@ -50,7 +51,7 @@ func (b *Bridge) GenerateContent(ctx context.Context, req *model.LLMRequest, str func (b *Bridge) generateStream(ctx context.Context, req *model.LLMRequest) iter.Seq2[*model.LLMResponse, error] { switch b.cfg.Provider { - case llmconfig.ProviderOpenAI: + case llmconfig.ProviderOpenAI, llmconfig.ProviderOrcaRouter: return b.generateOpenAIStream(ctx, req) case llmconfig.ProviderAnthropic: return b.generateAnthropicStream(ctx, req) @@ -63,7 +64,7 @@ func (b *Bridge) generateStream(ctx context.Context, req *model.LLMRequest) iter func (b *Bridge) generate(ctx context.Context, req *model.LLMRequest) (*model.LLMResponse, error) { switch b.cfg.Provider { - case llmconfig.ProviderOpenAI: + case llmconfig.ProviderOpenAI, llmconfig.ProviderOrcaRouter: return b.generateOpenAI(ctx, req) case llmconfig.ProviderAnthropic: return b.generateAnthropic(ctx, req) diff --git a/examples/kagent-demo/internal/llmbridge/bridge_test.go b/examples/kagent-demo/internal/llmbridge/bridge_test.go index 251a75d7b..09fa96792 100644 --- a/examples/kagent-demo/internal/llmbridge/bridge_test.go +++ b/examples/kagent-demo/internal/llmbridge/bridge_test.go @@ -75,6 +75,52 @@ func TestOpenAIBridgeSendsToolsAndParsesToolCall(t *testing.T) { } } +func TestOrcaRouterBridgeUsesOpenAICompatiblePath(t *testing.T) { + var captured map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/chat/completions" { + t.Fatalf("path = %q, want /chat/completions", r.URL.Path) + } + if got := r.Header.Get("Authorization"); got != "Bearer sk-orca-test" { + t.Fatalf("Authorization = %q", got) + } + if err := json.NewDecoder(r.Body).Decode(&captured); err != nil { + t.Fatalf("decode request: %v", err) + } + _, _ = w.Write([]byte(`{ + "choices": [{ + "message": { + "role": "assistant", + "content": "runbook lookup complete" + }, + "finish_reason": "stop" + }], + "usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8} + }`)) + })) + defer server.Close() + + llm := New(llmconfig.Config{ + Provider: ProviderOrcaRouter, + BaseURL: server.URL, + APIKey: "sk-orca-test", + Model: "orcarouter/auto", + Timeout: time.Second, + }, server.Client()) + + resp, err := firstResponse(llm.GenerateContent(context.Background(), sampleRequest(), false)) + if err != nil { + t.Fatalf("GenerateContent returned error: %v", err) + } + + if captured["model"] != "orcarouter/auto" { + t.Fatalf("model = %v", captured["model"]) + } + if got := resp.Content.Parts[0].Text; got != "runbook lookup complete" { + t.Fatalf("response text = %q", got) + } +} + func TestAnthropicBridgeSendsToolsAndParsesToolUse(t *testing.T) { var captured map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/examples/kagent-demo/internal/llmconfig/config.go b/examples/kagent-demo/internal/llmconfig/config.go index 8d5611730..216d67ab4 100644 --- a/examples/kagent-demo/internal/llmconfig/config.go +++ b/examples/kagent-demo/internal/llmconfig/config.go @@ -10,8 +10,9 @@ import ( type Provider string const ( - ProviderOpenAI Provider = "openai" - ProviderAnthropic Provider = "anthropic" + ProviderOpenAI Provider = "openai" + ProviderAnthropic Provider = "anthropic" + ProviderOrcaRouter Provider = "orcarouter" ) type Config struct { @@ -48,7 +49,7 @@ func LoadFromMap(env map[string]string) (Config, error) { return Config{}, fmt.Errorf("MODEL_PROVIDER is required") } switch cfg.Provider { - case ProviderOpenAI, ProviderAnthropic: + case ProviderOpenAI, ProviderAnthropic, ProviderOrcaRouter: default: return Config{}, fmt.Errorf("unsupported MODEL_PROVIDER %q", cfg.Provider) } diff --git a/examples/kagent-demo/internal/llmconfig/config_test.go b/examples/kagent-demo/internal/llmconfig/config_test.go index 2ea72f60a..6e348fbd1 100644 --- a/examples/kagent-demo/internal/llmconfig/config_test.go +++ b/examples/kagent-demo/internal/llmconfig/config_test.go @@ -45,6 +45,28 @@ func TestLoadAnthropicConfig(t *testing.T) { } } +func TestLoadOrcaRouterConfig(t *testing.T) { + env := map[string]string{ + "MODEL_PROVIDER": "orcarouter", + "MODEL_BASE_URL": "https://api.orcarouter.ai/v1", + "MODEL_API_KEY": "secret", + "MODEL_NAME": "orcarouter/auto", + "MODEL_TIMEOUT": "45s", + } + + got, err := LoadFromMap(env) + if err != nil { + t.Fatalf("LoadFromMap returned error: %v", err) + } + + if got.Provider != ProviderOrcaRouter { + t.Fatalf("Provider = %q, want %q", got.Provider, ProviderOrcaRouter) + } + if got.Timeout.String() != "45s" { + t.Fatalf("Timeout = %s, want 45s", got.Timeout) + } +} + func TestLoadRejectsMissingRequiredConfig(t *testing.T) { _, err := LoadFromMap(map[string]string{"MODEL_PROVIDER": "openai"}) if err == nil { -- Gitee