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.
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:
initializereturns the full playbook asinstructions(distributed fromGET /protocolwith the live configuration baked in). Put it in Converse'ssystemtext — it is re-sent every turn, so it belongs behind acachePointblock.- Each
tools/listentry 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 totools/calland return the text as atoolResult. Loop untilend_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:
| Model | Floor |
|---|---|
| text-embedding-3-large | 0.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 allentitlementAvailabilityauthorizationStatus— 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": "*"
}
]
}
Taguru