Taguru
deploy · amazon bedrock

Taguru on Amazon Bedrock

Taguru is model-agnostic — the server speaks HTTP, the bridge speaks MCP over stdio, and neither cares which model drives the agent. This page records the three integration points and the decisions each one forces.

Agent: any Converse-compatible model Embeddings: Titan / Cohere via InvokeModel Bridge: an OpenAI-compatible proxy

1. The agent side — Converse + taguru-mcp

MCP where MCP reaches; the HTTP API remains where it doesn't.

Using Claude Code on Bedrock (CLAUDE_CODE_USE_BEDROCK=1) needs nothing special — the MCP server is a local child process, so the claude mcp add taguru … of the normal setup works as-is.

Building your own agent loop on the Converse API, hold taguru-mcp as a stdio child process and translate mechanically:

  • initialize returns the full playbook as instructions (distributed from GET /protocol with the live configuration baked in). Put it in Converse's system text — it is re-sent every turn, so it belongs behind a cachePoint block.
  • Each tools/list entry maps 1:1 onto a Converse tool: {"toolSpec": {"name", "description", "inputSchema": {"json": <the MCP inputSchema>}}} — the schemas are plain JSON Schema and pass through unmodified.
  • On stopReason == "tool_use", forward each block to tools/call and return the text as a toolResult. Loop until end_turn.

Managed agent runtimes (Bedrock AgentCore and the like) want a remote MCP endpoint — that exists too: POST /mcp speaks MCP Streamable HTTP behind the same Bearer token as the rest of the API (TLS via a reverse proxy). Register https://your-host/mcp directly. The stdio bridge remains the right answer for co-located setups, and where MCP doesn't reach at all, the HTTP API fits an OpenAPI action group / Gateway target as a small Bearer-authenticated JSON API.

2. Embeddings — a Bedrock model behind a bridge proxy

Taguru speaks one embedding protocol: OpenAI-compatible POST {model, input}{data: [{embedding}]}. Bedrock's embedding models sit behind InvokeModel + SigV4, with a per-family body shape.

Bridge with LiteLLM or AWS's Bedrock Access Gateway sample (both already speak OpenAI) — or a proxy this small is enough (error handling omitted; bind to loopback only):

#!/usr/bin/env python3
# OpenAI-compatible /embeddings → Bedrock InvokeModel (Titan & Cohere)
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
import boto3

bedrock = boto3.Session(region_name="us-east-1").client("bedrock-runtime")

def invoke(model_id, body):
    response = bedrock.invoke_model(modelId=model_id, body=json.dumps(body))
    return json.loads(response["body"].read())

def embed(model_id, texts, purpose):
    if model_id.startswith("cohere.embed"):
        # X-Taguru-Embed-Purpose is exactly the asymmetry Cohere asks for
        input_type = "search_document" if purpose == "index" else "search_query"
        out = []
        for i in range(0, len(texts), 96):  # Cohere caps at 96 texts
            out += invoke(model_id, {"texts": texts[i : i + 96],
                                     "input_type": input_type})["embeddings"]
        return out
    shape = (lambda t: {"inputText": t, "dimensions": 512}) if "v2" in model_id \
        else (lambda t: {"inputText": t})  # Titan: one text per call
    return [invoke(model_id, shape(t))["embedding"] for t in texts]

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        request = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        texts = request["input"]
        texts = [texts] if isinstance(texts, str) else texts
        purpose = self.headers.get("X-Taguru-Embed-Purpose", "query")
        vectors = embed(request["model"], texts, purpose)
        body = json.dumps({"data":
            [{"embedding": v, "index": i} for i, v in enumerate(vectors)]}).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

HTTPServer(("127.0.0.1", 8257), Handler).serve_forever()

Point Taguru at it:

TAGURU_EMBED_URL=http://127.0.0.1:8257/v1/embeddings
TAGURU_EMBED_MODEL=amazon.titan-embed-text-v2:0
TAGURU_SEMANTIC_FLOOR=0.2   # calibrated below — do not skip this
TAGURU_EMBED_AUTO=1         # don't count on the agent calling refresh

Taguru normalizes the vectors it receives, so Titan's raw (unnormalized) output is fine as-is. A changed dimensions setting behind the same model name is detected, not trusted: each vector sidecar records the (model, width) it was built with, and mismatched vectors are never served. The search response's plan and the search/explain endpoint name the mismatch outright; a plain resolve still answers [] (exactly like a model change — its semantic tier is best-effort), so ask resolve/explain, which names it too. The next embeddings/refresh discards and re-embeds the store (taguru_embedding_width_rebuilds_total counts these). Still pick one width and keep it: the rebuild is automatic, but it re-buys every vector at provider prices, and the calibrated floor below is width-specific too.

The bridge proxy is one more thing that can go down — SigV4 expiring, the Titan/Cohere quota throttling, the process above simply crashing. Taguru doesn't let a stuck bridge stall every caller on the full provider timeout: three consecutive failed calls open a circuit breaker, and while it's open every embedding attempt fails fast instead of waiting — sources/search falls back to its lexical lane and resolve answers from lexical candidates or embeddings_failed, exactly as they do with no TAGURU_EMBED_URL configured at all. A single probe call after a 30s cooldown decides whether to close it again. Watch taguru_embedding_breaker_state (and _consecutive_failures / _opened_total / _short_circuits_total) on /metrics to tell "the bridge is down" apart from "every query happens to be below the floor."

3. Calibrating TAGURU_SEMANTIC_FLOOR

The floor is a property of the embedding model. Switching models while keeping the default can silently discard everything.

The built-in floor (0.35) is calibrated for text-embedding-3-large over glosses. Titan V2 (512 dimensions) compresses cosines on Japanese text: true matches typically land around 0.2–0.3 with unrelated names near ~0.15, so the default 0.35 can silently discard every correct answer. The symptom: resolve returns [] while the ranking underneath is perfect.

Calibrate once per embedding model — the ritual is mechanical, so the CLI runs it:

taguru calibrate --context sake --probes probes.tsv http://127.0.0.1:8248

The probe file is TSV — cue<TAB>expected, one per line, where the cue is a paraphrase sharing no spelling with any stored name and expected is the stored concept it should reach. Ingest representative content and refresh embeddings first. For each probe, the expected name's own gloss cosine feeds the upper band and the best other candidate feeds the lower; the report prints both bands, the gap, and a suggested TAGURU_SEMANTIC_FLOOR mid-gap (--json for automation), stamped with the (model, width) it measured — the identity GET /contexts/{name}/embeddings serves. Cues that lexically resolve — the step humans get wrong — are detected and excluded loudly, and overlapping bands are reported as exactly that (a model that cannot separate these names at this dimension), never papered over with a number. The manual probe, when you want to see one score with your own eyes: POST /contexts/{name}/resolve {"cue": "…", "semantic_floor": 0.05}.

Starting points per model — always confirm with taguru calibrate before trusting one:

ModelFloor
text-embedding-3-large0.35 (the default)
amazon.titan-embed-text-v2:0 (512d, Japanese)~0.2

4. When Bedrock refuses to serve a model

Access to third-party models can flap — the same request succeeds and is then denied while console-side state propagates. Skip the guessing and name every gate in one call.

aws bedrock get-foundation-model-availability \
  --model-id anthropic.claude-sonnet-4-5-20250929-v1:0
  • regionAvailability — is it offered in this region at all
  • entitlementAvailability
  • authorizationStatus — Anthropic's use-case form (in the console, once per account; for an individual, your name and a GitHub URL suffice)
  • agreementAvailability — the AWS Marketplace agreement

The one that resists console repair is agreementAvailability: NOT_AVAILABLE (invocation errors keep blaming IAM while the console shows nothing pending). Create the agreement directly:

aws bedrock list-foundation-model-agreement-offers --model-id <id>   # → offerToken
aws bedrock create-foundation-model-agreement --model-id <id> --offer-token <token>

Bedrock model offers are $0-upfront, pay-as-you-go. Creating the agreement needs aws-marketplace:Subscribe + aws-marketplace:ViewSubscriptions on the calling identity, once — the grant can be peeled off after it succeeds, and ordinary calls thereafter need only bedrock:InvokeModel.

Two more traps:

  • Newer Claude models refuse the bare model ID ("on-demand throughput isn't supported"): call the cross-region inference profile (us./apac./global. prefix), and grant IAM on both the inference-profile ARN and the foundation-model ARNs of every region it can route to — wildcard the region.
  • Amazon's own models (Titan) don't pass through the Marketplace. If Titan works while Anthropic/Cohere fail, it's the agreement gate, not your policy.

A minimal invocation policy

(Does not include agreement creation — grant that separately, briefly.)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
      "Resource": [
        "arn:aws:bedrock:*::foundation-model/anthropic.*",
        "arn:aws:bedrock:*::foundation-model/amazon.titan-embed-*",
        "arn:aws:bedrock:*::foundation-model/cohere.embed-*",
        "arn:aws:bedrock:*:<ACCOUNT_ID>:inference-profile/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "bedrock:ListFoundationModels",
        "bedrock:GetFoundationModel",
        "bedrock:GetFoundationModelAvailability",
        "bedrock:ListInferenceProfiles",
        "bedrock:GetInferenceProfile"
      ],
      "Resource": "*"
    }
  ]
}