From 9891bf4c61474761fa5d42aedb4c477fb2eb194b Mon Sep 17 00:00:00 2001 From: Osher-Elhadad Date: Tue, 21 Jul 2026 14:45:30 +0300 Subject: [PATCH 1/3] feat(proxy): add Bob (BobShell) gateway routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bob is OpenAI-compatible but calls Bob-specific paths. Add an opt-in gateway (enabled by BOB_UPSTREAM / --bob-upstream): - POST /inference/v1/chat/completions is reduced through the pipeline like any OpenAI chat and forwarded to the Bob backend, passing Bob's own auth through. - A "/" catch-all transparently proxies Bob's control-plane calls (/admin/v1/profile, /inference/v1/model/info, …) verbatim so the CLI boots and authenticates. Less specific than every existing route, and only registered when BOB_UPSTREAM is set, so default behavior is unchanged. Point Bob's CUSTOM_BASE_URL at this proxy. Verified end-to-end against the Bob backend with deterministic components. Ports winnow's bob profile. Assisted-By: Claude Signed-off-by: Osher-Elhadad --- cmd/context-guru-proxy/main.go | 2 + proxy/proxy.go | 54 +++++++++++++++++++++++-- proxy/proxy_test.go | 72 ++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 3 deletions(-) diff --git a/cmd/context-guru-proxy/main.go b/cmd/context-guru-proxy/main.go index 3f5231e..06958b4 100644 --- a/cmd/context-guru-proxy/main.go +++ b/cmd/context-guru-proxy/main.go @@ -33,6 +33,7 @@ func main() { preset = flag.String("preset", envOr("PRESET", "balanced"), "preset to use when --config is absent") openai = flag.String("openai-upstream", envOr("OPENAI_UPSTREAM", "https://api.openai.com"), "OpenAI upstream base URL") anthropic = flag.String("anthropic-upstream", envOr("ANTHROPIC_UPSTREAM", "https://api.anthropic.com"), "Anthropic upstream base URL") + bob = flag.String("bob-upstream", envOr("BOB_UPSTREAM", ""), "Bob (BobShell) backend base URL; enables the Bob gateway routes when set (e.g. https://api.us-east.bob.ibm.com)") storeFlag = flag.String("store", envOr("STORE", ""), "override state store: true|false (default: config store.enabled, else on)") ) flag.Parse() @@ -55,6 +56,7 @@ func main() { h := proxy.New(pipe, cfg.NewStore(), agg, proxy.Options{ OpenAIUpstream: *openai, AnthropicUpstream: *anthropic, + BobUpstream: *bob, // enables the Bob gateway routes when set (BOB_UPSTREAM) // Gateway mode: real provider keys live here (eval-containers passes them // via env); the agent holds only a placeholder. Empty => pass client auth. OpenAIKey: os.Getenv("OPENAI_API_KEY"), diff --git a/proxy/proxy.go b/proxy/proxy.go index 8c7b873..631915b 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -18,6 +18,7 @@ import ( "strings" "time" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/apply" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/expand" @@ -25,7 +26,6 @@ import ( "github.com/rossoctl/context-guru/metrics" "github.com/rossoctl/context-guru/schema" "github.com/rossoctl/context-guru/store" - bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -39,8 +39,14 @@ import ( type Options struct { OpenAIUpstream string // e.g. https://api.openai.com AnthropicUpstream string - OpenAIKey string // injected as Authorization: Bearer - AnthropicKey string // injected as x-api-key: + // BobUpstream, when set, enables the Bob (BobShell) gateway: Bob's + // OpenAI-dialect model calls (POST /inference/v1/chat/completions) are reduced + // and forwarded here, and every other path Bob calls (control-plane: + // /admin/v1/profile, /inference/v1/model/info, …) is proxied through verbatim + // so the CLI boots and authenticates. Point Bob's CUSTOM_BASE_URL at this proxy. + BobUpstream string // e.g. https://api.us-east.bob.ibm.com + OpenAIKey string // injected as Authorization: Bearer + AnthropicKey string // injected as x-api-key: // ForceModel, when set, overwrites the request's "model" field. eval-containers // uses this to pin every call to EVAL_MODEL regardless of what the agent asked for. ForceModel string @@ -100,9 +106,51 @@ func (h *Handler) Mux() *http.ServeMux { m.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) }) m.HandleFunc("GET /stats", h.stats) m.HandleFunc("GET /expand", h.expand) + // Bob (BobShell) gateway. Bob is OpenAI-compatible but calls Bob-specific + // paths: its model call is POST /inference/v1/chat/completions (reduced like + // any OpenAI chat), and its control-plane calls (/admin/v1/profile, + // /inference/v1/model/info, …) must pass through verbatim so the CLI boots and + // authenticates. The "/" catch-all is less specific than every route above, so + // it only receives what nothing else matched. Enabled only when BobUpstream is + // set, so default proxy behavior (unknown path => 404) is unchanged. + if h.opts.BobUpstream != "" { + m.HandleFunc("POST /inference/v1/chat/completions", h.chat(bschemas.OpenAI, upstream{ + base: h.opts.BobUpstream, + path: "/inference/v1/chat/completions", + // setKey nil: pass Bob's own auth (BOBSHELL key) straight through. + })) + m.HandleFunc("/", h.passthrough(h.opts.BobUpstream)) + } return m } +// passthrough transparently forwards a request to the Bob upstream unchanged — +// for Bob's control-plane calls that must not be rewritten. Bob's own auth +// header passes straight through (no key injection); the response is streamed +// back as-is. Only the model route is reduced; everything else Bob calls lands +// here and is proxied verbatim. +func (h *Handler) passthrough(base string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + target := base + r.URL.Path + if r.URL.RawQuery != "" { + target += "?" + r.URL.RawQuery + } + req, err := http.NewRequestWithContext(r.Context(), r.Method, target, strings.NewReader(string(body))) + if err != nil { + http.Error(w, "proxy: "+err.Error(), http.StatusBadGateway) + return + } + copyHeaders(req.Header, r.Header) + resp, err := h.client.Do(req) + if err != nil { + http.Error(w, "upstream: "+err.Error(), http.StatusBadGateway) + return + } + h.stream(w, resp) + } +} + // compact runs the pipeline over the request body's messages and returns the // rewritten body — without forwarding upstream. This is the "compact a context, // hand it back" endpoint: a caller (e.g. the llm-d-router request-inline- diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 1372340..fbf6021 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -129,6 +129,78 @@ func TestAnthropicRouteReducesToolResult(t *testing.T) { } } +// TestBobGatewayReducesModelAndPassesControlPlane drives the Bob (BobShell) +// gateway: the OpenAI-dialect model call on /inference/v1/chat/completions is +// reduced like any chat and forwarded to the same path, while a control-plane +// call (GET /admin/v1/profile) passes through to the upstream verbatim. +func TestBobGatewayReducesModelAndPassesControlPlane(t *testing.T) { + type hit struct { + method, path string + body []byte + } + var hits []hit + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + hits = append(hits, hit{r.Method, r.URL.Path, b}) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"ok":true}`)) + })) + defer upstream.Close() + + cfg, err := config.LoadBytes([]byte("pipeline: [dedup]\ncomponents:\n dedup: {min_tokens: 20}\n")) + if err != nil { + t.Fatal(err) + } + agg := metrics.NewAggregator() + pipe, err := cfg.Build(agg) + if err != nil { + t.Fatal(err) + } + h := proxy.New(pipe, store.NewMemory(store.Options{}), agg, proxy.Options{BobUpstream: upstream.URL}) + srv := httptest.NewServer(h.Mux()) + defer srv.Close() + + // 1) Model call on Bob's path is reduced (dedup) and forwarded to the same path. + dump := strings.Repeat("verbose repeated bob tool output line\n", 40) + body := openAIBody( + map[string]any{"role": "user", "content": "do it"}, + map[string]any{"role": "tool", "tool_call_id": "a", "content": dump}, + map[string]any{"role": "tool", "tool_call_id": "b", "content": dump}, + ) + resp, err := http.Post(srv.URL+"/inference/v1/chat/completions", "application/json", strings.NewReader(string(body))) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + + // 2) Control-plane call is proxied through verbatim. + cp, err := http.Get(srv.URL + "/admin/v1/profile") + if err != nil { + t.Fatal(err) + } + cp.Body.Close() + + if len(hits) != 2 { + t.Fatalf("want 2 upstream hits, got %d: %+v", len(hits), hits) + } + model, control := hits[0], hits[1] + if model.path != "/inference/v1/chat/completions" { + t.Fatalf("model call forwarded to wrong path: %q", model.path) + } + if !strings.Contains(gjson.GetBytes(model.body, "messages.2.content").String(), "identical to an earlier") { + t.Fatalf("dedup did not run on the bob model call: %s", model.body) + } + if len(model.body) >= len(body) { + t.Fatalf("bob model call not shrunk (before=%d after=%d)", len(body), len(model.body)) + } + if control.method != "GET" || control.path != "/admin/v1/profile" { + t.Fatalf("control-plane not passed through verbatim: %s %s", control.method, control.path) + } + if len(control.body) != 0 { + t.Fatalf("control-plane GET should have empty body, got %d bytes", len(control.body)) + } +} + func TestBypassHeaderForwardsUnchanged(t *testing.T) { var got []byte upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From 27f777364be61ce72d38be1126bfd785656b344d Mon Sep 17 00:00:00 2001 From: Osher-Elhadad Date: Tue, 21 Jul 2026 14:57:25 +0300 Subject: [PATCH 2/3] chore: gofmt imports after rossoctl module rename The kagenti->rossoctl rename string-replaced import paths without re-sorting, so github.com/rossoctl/... now sorts after github.com/maximhq/...; gofmt re-orders the import groups. Fixes `make lint` (gofmt check) on this branch. Assisted-By: Claude Signed-off-by: Osher-Elhadad --- adapters/bifrost/plugin.go | 2 +- adapters/bifrost/plugin_test.go | 2 +- apply/apply.go | 2 +- apply/apply_test.go | 2 +- components/all/all_test.go | 2 +- components/all/llm_test.go | 2 +- components/all/markermode_test.go | 2 +- components/all/more_test.go | 2 +- components/all/p4_test.go | 2 +- components/all/reuse_test.go | 2 +- components/all/skeleton_test.go | 2 +- components/component.go | 2 +- components/offload/cmdfilter.go | 2 +- components/offload/collapse.go | 2 +- components/offload/common.go | 2 +- components/offload/dedup.go | 2 +- components/offload/extract.go | 2 +- components/offload/failed_run.go | 2 +- components/offload/mask.go | 2 +- components/offload/phi_evict.go | 2 +- components/offload/skeleton.go | 2 +- components/offload/smartcrush.go | 2 +- components/offload/state.go | 2 +- components/offload/summarize.go | 2 +- components/offload/zz_live_test.go | 2 +- components/pipeline.go | 2 +- components/pipeline_test.go | 2 +- components/reformat/cacheinject.go | 2 +- components/reformat/format.go | 2 +- components/reformat/toon.go | 2 +- components/trigger.go | 2 +- schema/schema.go | 2 +- 32 files changed, 32 insertions(+), 32 deletions(-) diff --git a/adapters/bifrost/plugin.go b/adapters/bifrost/plugin.go index 465ba0f..ecfacef 100644 --- a/adapters/bifrost/plugin.go +++ b/adapters/bifrost/plugin.go @@ -8,11 +8,11 @@ package bifrost import ( + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/schema" "github.com/rossoctl/context-guru/session" "github.com/rossoctl/context-guru/store" - bschemas "github.com/maximhq/bifrost/core/schemas" ) // SessionContextKey is the BifrostContext value key the transport sets from the diff --git a/adapters/bifrost/plugin_test.go b/adapters/bifrost/plugin_test.go index 7de4e1b..e7d6fab 100644 --- a/adapters/bifrost/plugin_test.go +++ b/adapters/bifrost/plugin_test.go @@ -6,11 +6,11 @@ import ( "testing" "time" + bschemas "github.com/maximhq/bifrost/core/schemas" _ "github.com/rossoctl/context-guru/components/all" "github.com/rossoctl/context-guru/config" "github.com/rossoctl/context-guru/schema" "github.com/rossoctl/context-guru/store" - bschemas "github.com/maximhq/bifrost/core/schemas" ) func toolMsg(text string) bschemas.ChatMessage { diff --git a/apply/apply.go b/apply/apply.go index a6813e2..abffd81 100644 --- a/apply/apply.go +++ b/apply/apply.go @@ -27,11 +27,11 @@ import ( "strconv" "strings" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/schema" "github.com/rossoctl/context-guru/session" "github.com/rossoctl/context-guru/store" - bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) diff --git a/apply/apply_test.go b/apply/apply_test.go index bd8a915..d63a903 100644 --- a/apply/apply_test.go +++ b/apply/apply_test.go @@ -6,12 +6,12 @@ import ( "strings" "testing" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/apply" "github.com/rossoctl/context-guru/components" _ "github.com/rossoctl/context-guru/components/all" "github.com/rossoctl/context-guru/config" "github.com/rossoctl/context-guru/store" - bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/tidwall/gjson" ) diff --git a/components/all/all_test.go b/components/all/all_test.go index 798780c..1c7d3ba 100644 --- a/components/all/all_test.go +++ b/components/all/all_test.go @@ -5,13 +5,13 @@ import ( "strings" "testing" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" _ "github.com/rossoctl/context-guru/components/all" "github.com/rossoctl/context-guru/config" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" "github.com/rossoctl/context-guru/store" - "github.com/maximhq/bifrost/core/schemas" ) func toolMsg(text string) schemas.ChatMessage { diff --git a/components/all/llm_test.go b/components/all/llm_test.go index 16d5c00..e57f524 100644 --- a/components/all/llm_test.go +++ b/components/all/llm_test.go @@ -6,10 +6,10 @@ import ( "strings" "testing" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/schema" "github.com/rossoctl/context-guru/store" - bschemas "github.com/maximhq/bifrost/core/schemas" ) // stubModel is a fixed LLM used to drive the model-based components in tests. diff --git a/components/all/markermode_test.go b/components/all/markermode_test.go index 1ea7fd3..00f03ee 100644 --- a/components/all/markermode_test.go +++ b/components/all/markermode_test.go @@ -7,12 +7,12 @@ import ( "strings" "testing" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/apply" "github.com/rossoctl/context-guru/config" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" "github.com/rossoctl/context-guru/store" - "github.com/maximhq/bifrost/core/schemas" "github.com/tidwall/gjson" ) diff --git a/components/all/more_test.go b/components/all/more_test.go index 045293f..de2152e 100644 --- a/components/all/more_test.go +++ b/components/all/more_test.go @@ -6,13 +6,13 @@ import ( "strings" "testing" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" _ "github.com/rossoctl/context-guru/components/all" "github.com/rossoctl/context-guru/config" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" "github.com/rossoctl/context-guru/store" - "github.com/maximhq/bifrost/core/schemas" ) func run(t *testing.T, yaml string, req *schemas.BifrostChatRequest) (*components.RunReport, store.Store) { diff --git a/components/all/p4_test.go b/components/all/p4_test.go index 622fccc..b4c956a 100644 --- a/components/all/p4_test.go +++ b/components/all/p4_test.go @@ -6,9 +6,9 @@ import ( "strings" "testing" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" - bschemas "github.com/maximhq/bifrost/core/schemas" ) func userMsg(text string) bschemas.ChatMessage { diff --git a/components/all/reuse_test.go b/components/all/reuse_test.go index 9b2edfc..076fba7 100644 --- a/components/all/reuse_test.go +++ b/components/all/reuse_test.go @@ -6,10 +6,10 @@ import ( "sync" "testing" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/schema" "github.com/rossoctl/context-guru/store" - bschemas "github.com/maximhq/bifrost/core/schemas" ) // countingModel is a stubModel that records how many times it was called, so a diff --git a/components/all/skeleton_test.go b/components/all/skeleton_test.go index 01479fb..17e4e8b 100644 --- a/components/all/skeleton_test.go +++ b/components/all/skeleton_test.go @@ -11,9 +11,9 @@ import ( "strings" "testing" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" - "github.com/maximhq/bifrost/core/schemas" ) func TestSkeletonElidesBodies(t *testing.T) { diff --git a/components/component.go b/components/component.go index 06ef8a3..ef1ed5c 100644 --- a/components/component.go +++ b/components/component.go @@ -21,8 +21,8 @@ import ( "context" "time" - "github.com/rossoctl/context-guru/store" "github.com/maximhq/bifrost/core/schemas" + "github.com/rossoctl/context-guru/store" ) // Component is the common surface: identity + a per-request enable check. diff --git a/components/offload/cmdfilter.go b/components/offload/cmdfilter.go index 0d67d90..338205f 100644 --- a/components/offload/cmdfilter.go +++ b/components/offload/cmdfilter.go @@ -8,11 +8,11 @@ import ( "encoding/hex" "strings" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/components/dsl" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" - "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/offload/collapse.go b/components/offload/collapse.go index 7ff736a..76ffe8c 100644 --- a/components/offload/collapse.go +++ b/components/offload/collapse.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" - "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/offload/common.go b/components/offload/common.go index 8b8599b..93140c1 100644 --- a/components/offload/common.go +++ b/components/offload/common.go @@ -3,8 +3,8 @@ package offload import ( "strings" - "github.com/rossoctl/context-guru/schema" bschemas "github.com/maximhq/bifrost/core/schemas" + "github.com/rossoctl/context-guru/schema" ) // errWords mark a tool output (or an item) as carrying a failure — such items diff --git a/components/offload/dedup.go b/components/offload/dedup.go index 88e1ca7..002e2af 100644 --- a/components/offload/dedup.go +++ b/components/offload/dedup.go @@ -1,9 +1,9 @@ package offload import ( + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/schema" - "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/offload/extract.go b/components/offload/extract.go index 98d2778..bdd96e7 100644 --- a/components/offload/extract.go +++ b/components/offload/extract.go @@ -5,11 +5,11 @@ import ( "strings" "time" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/internal/extract" "github.com/rossoctl/context-guru/schema" - bschemas "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/offload/failed_run.go b/components/offload/failed_run.go index 5395dd3..3db3b4a 100644 --- a/components/offload/failed_run.go +++ b/components/offload/failed_run.go @@ -3,10 +3,10 @@ package offload import ( "regexp" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" - "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/offload/mask.go b/components/offload/mask.go index 2a8c066..8f711cd 100644 --- a/components/offload/mask.go +++ b/components/offload/mask.go @@ -1,10 +1,10 @@ package offload import ( + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" - bschemas "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/offload/phi_evict.go b/components/offload/phi_evict.go index a396e00..920ccca 100644 --- a/components/offload/phi_evict.go +++ b/components/offload/phi_evict.go @@ -3,10 +3,10 @@ package offload import ( "sort" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" - bschemas "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/offload/skeleton.go b/components/offload/skeleton.go index 6671b4a..765fd74 100644 --- a/components/offload/skeleton.go +++ b/components/offload/skeleton.go @@ -13,11 +13,11 @@ import ( "regexp" "strings" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/internal/treesitter" "github.com/rossoctl/context-guru/schema" - "github.com/maximhq/bifrost/core/schemas" sitter "github.com/tree-sitter/go-tree-sitter" "gopkg.in/yaml.v3" ) diff --git a/components/offload/smartcrush.go b/components/offload/smartcrush.go index 1c1c241..fd59a6e 100644 --- a/components/offload/smartcrush.go +++ b/components/offload/smartcrush.go @@ -5,10 +5,10 @@ import ( "fmt" "strings" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" - bschemas "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/offload/state.go b/components/offload/state.go index 1e9360e..33ed3ef 100644 --- a/components/offload/state.go +++ b/components/offload/state.go @@ -5,8 +5,8 @@ import ( "encoding/hex" "encoding/json" - "github.com/rossoctl/context-guru/components" bschemas "github.com/maximhq/bifrost/core/schemas" + "github.com/rossoctl/context-guru/components" ) // State reuse over the generic Store (key→bytes), session-scoped by key prefix diff --git a/components/offload/summarize.go b/components/offload/summarize.go index 1f8f632..101f0ce 100644 --- a/components/offload/summarize.go +++ b/components/offload/summarize.go @@ -6,10 +6,10 @@ import ( "strings" "time" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/expand" "github.com/rossoctl/context-guru/schema" - bschemas "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/offload/zz_live_test.go b/components/offload/zz_live_test.go index 7584479..c4282be 100644 --- a/components/offload/zz_live_test.go +++ b/components/offload/zz_live_test.go @@ -6,9 +6,9 @@ import ( "strings" "testing" + bschemas "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/internal/cheapmodel" "github.com/rossoctl/context-guru/schema" - bschemas "github.com/maximhq/bifrost/core/schemas" ) // Live example of the summarize component: real model compresses a realistic diff --git a/components/pipeline.go b/components/pipeline.go index cb7c129..5c246e3 100644 --- a/components/pipeline.go +++ b/components/pipeline.go @@ -3,8 +3,8 @@ package components import ( "fmt" - "github.com/rossoctl/context-guru/schema" "github.com/maximhq/bifrost/core/schemas" + "github.com/rossoctl/context-guru/schema" ) // Pipeline runs an ordered list of components over a request. Order is set by diff --git a/components/pipeline_test.go b/components/pipeline_test.go index 60bea51..8611f67 100644 --- a/components/pipeline_test.go +++ b/components/pipeline_test.go @@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/rossoctl/context-guru/store" "github.com/maximhq/bifrost/core/schemas" + "github.com/rossoctl/context-guru/store" ) // --- fakes --- diff --git a/components/reformat/cacheinject.go b/components/reformat/cacheinject.go index 803c084..4351749 100644 --- a/components/reformat/cacheinject.go +++ b/components/reformat/cacheinject.go @@ -4,8 +4,8 @@ package reformat import ( - "github.com/rossoctl/context-guru/components" "github.com/maximhq/bifrost/core/schemas" + "github.com/rossoctl/context-guru/components" ) func init() { components.Register("cacheinject", newCacheinject) } diff --git a/components/reformat/format.go b/components/reformat/format.go index 6744606..f53a89a 100644 --- a/components/reformat/format.go +++ b/components/reformat/format.go @@ -4,9 +4,9 @@ import ( "encoding/json" "strings" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/schema" - "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/reformat/toon.go b/components/reformat/toon.go index 9bc2b4b..5c44628 100644 --- a/components/reformat/toon.go +++ b/components/reformat/toon.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" + "github.com/maximhq/bifrost/core/schemas" "github.com/rossoctl/context-guru/components" "github.com/rossoctl/context-guru/schema" - "github.com/maximhq/bifrost/core/schemas" "gopkg.in/yaml.v3" ) diff --git a/components/trigger.go b/components/trigger.go index 12e513f..3f8cd72 100644 --- a/components/trigger.go +++ b/components/trigger.go @@ -1,8 +1,8 @@ package components import ( - "github.com/rossoctl/context-guru/schema" "github.com/maximhq/bifrost/core/schemas" + "github.com/rossoctl/context-guru/schema" ) // Trigger is the shared, configurable gate that decides whether an expensive diff --git a/schema/schema.go b/schema/schema.go index 4e1ad35..6cf1485 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -13,8 +13,8 @@ package schema import ( "encoding/json" - "github.com/rossoctl/context-guru/internal/tokens" "github.com/maximhq/bifrost/core/schemas" + "github.com/rossoctl/context-guru/internal/tokens" ) // Provider identifies the wire dialect a request arrived in. It drives From 8cb9c1cc8a1f6dbd401264979bc07d5897ef3e0f Mon Sep 17 00:00:00 2001 From: Osher-Elhadad Date: Tue, 21 Jul 2026 14:57:26 +0300 Subject: [PATCH 3/3] docs: document the Bob (BobShell) gateway integration Add an integrations section covering the opt-in Bob gateway: enable with BOB_UPSTREAM, point Bob's CUSTOM_BASE_URL at the proxy, model calls reduced / control-plane passed through. Notes the verified deterministic setup. Assisted-By: Claude Signed-off-by: Osher-Elhadad --- docs/integrations.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/integrations.md b/docs/integrations.md index 13fb0a7..298a4b6 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -89,3 +89,45 @@ p := bifrost.New(pipe, store) // pipe from config.Build, store from config.New header / Anthropic `metadata.user_id`), else the content-hash fallback. - `PreLLMHook`/`PostLLMHook` are pass-throughs — the expand loop belongs in a transport wrapper (it must re-invoke upstream, which a hook cannot). + +## Use it with an agent — Bob (BobShell) + +IBM Bob is OpenAI-compatible via its `CUSTOM_BASE_URL`, but it calls **Bob-specific +paths**: its model call is `POST /inference/v1/chat/completions`, and it makes +control-plane calls (`GET /admin/v1/profile`, `/inference/v1/model/info`, …) that must +reach the backend unmodified or the CLI won't boot. The proxy has an opt-in **Bob +gateway** for exactly this shape — enable it with `BOB_UPSTREAM` (or `--bob-upstream`): + +```sh +BOB_UPSTREAM=https://api.us-east.bob.ibm.com \ + context-guru-proxy --preset balanced # any deterministic preset/config +``` + +Then point Bob at the proxy and let it use its own key: + +```sh +CUSTOM_BASE_URL=http://localhost:4000/v1 \ +BOBSHELL_DEFAULT_AUTH_TYPE=custom \ +BOBSHELL_API_KEY= \ + bob --yolo "your task" +``` + +How the gateway routes Bob's traffic: + +- **Model calls** (`/inference/v1/chat/completions`) run through the pipeline like any + OpenAI chat and are forwarded to the same path on `BOB_UPSTREAM`. Bob's own auth is + passed straight through (no key injection). +- **Control-plane calls** (everything else Bob hits) are proxied **verbatim** to + `BOB_UPSTREAM`, so Bob authenticates and starts normally. + +!!! tip "Start with deterministic components" + A lossless, LLM-free pipeline (`format`, `toon`, `dedup`, `failed_run`, `cmdfilter`) + needs no cheap-model config and leaves the transcript reversible. Verified end-to-end: + with `[format, toon]` Bob authenticates and answers correctly through the proxy, with + its model call reduced. For long Bob sessions, add `mask` (see [Choose a preset](../how-to/choose-a-preset.md)). + +!!! note "Bob speaks its own backend protocol" + Unlike Claude Code (`ANTHROPIC_BASE_URL`) or OpenAI-surface agents (`OPENAI_BASE_URL`), + Bob's `CUSTOM_BASE_URL` points at the proxy **host**; Bob supplies the `/inference` and + `/admin` paths itself. The gateway only activates when `BOB_UPSTREAM` is set, so it + never changes behavior for the other integrations above.