TL;DR
- Wire Zed’s open-source agent harness to Amazon Bedrock and the editor inherits your AWS account’s governance boundary — auth, audit, region, billing — instead of one provider integration per tool.
- The harness stays fixed; the model plane swaps underneath it. Claude, OpenAI GPT, xAI Grok, and open-weight models become config-change choices, not migrations.
- Native Bedrock (Converse) covers Claude. OpenAI and Grok live on a separate OpenAI-shaped endpoint and need a small local translation proxy — included in full below.
- Zed’s Business plan is a per-seat governance license that stacks on top; inference still runs in your own Bedrock account, model-agnostic either way.
Zed is not another VS Code skin with chat bolted on. It is a code editor from Zed Industries, the team that previously created Atom, Electron, and Tree-sitter. That history matters because Zed’s center of gravity is still the editor: low-latency text, collaboration, project context, and code-adjacent workflows that stay close to the files.
The AI layer follows the same shape. Zed’s AI docs separate agent paths from model access. The native Zed Agent is one way to run agentic work inside the editor. External agents and terminal-backed sessions are other ways. Model access is a separate decision: Zed-hosted models, provider APIs, gateways, subscriptions, local models, or Bedrock.
That makes Bedrock support interesting for a practical reason: Zed’s agent harness can run across the models your Bedrock account can reach. The harness surface is Zed. The model plane is Bedrock. The bill for inference is still the model bill, but the editor-side agent experience is no longer tied to a single hosted model plan.
That is the real unlock, and it is bigger than a personal convenience. The coding agent is now the highest-volume AI surface in most engineering orgs — in a16z’s 2025 survey of 100 enterprise CIOs, software development is called out as the killer use case, with one CTO reporting nearly 90% of new code generated through AI coding tools. Point that surface at a governed model plane and every request inherits the account’s auth boundary, audit trail, region controls, and billing — instead of scattering source code across one direct provider integration per tool. Claude can be the default today. OpenAI and xAI/Grok can become the same kind of choice when the account and endpoint expose them. The user keeps one editor harness and swaps the model plane underneath it.
The distinction still matters. Zed provides the native editor harness: agent panel, project context, file edits, terminal-adjacent work, and model selection. Bedrock provides the model boundary: account access, model availability, region behavior, inference profiles, auditability, and billing. Wiring Zed to Bedrock is powerful because those two layers line up cleanly — and because that boundary is exactly the thing enterprises are now scrambling to put in front of every model call.
The Clean Shape
Zed exposes three useful surfaces for agentic work. They can look similar from the sidebar. They are not the same system.
| Path | What Zed Owns | What The Agent Owns |
|---|---|---|
| Native Zed Agent on Bedrock | Editor harness, tools, model picker, thread surface | Bedrock owns inference for the selected model |
| ACP external agent | Editor integration and protocol boundary | Agent harness, model routing, tool policy, memory behavior |
| Terminal thread | Terminal surface inside Zed | The CLI’s native behavior |
This is where ACP belongs in the post: as boundary context. Zed Industries helped create the Agent Client Protocol, and ACP matters when Zed is hosting an external coding agent. But this setup is not an ACP setup. It is the simpler path: run the native Zed Agent with Bedrock as the model provider.
That table is the whole decision. If the goal is “use Zed’s agent harness across Bedrock models,” configure the native provider. If the goal is “preserve the behavior of a different coding harness,” run that harness directly or through ACP and accept that Zed is now a host, not the agent brain.
Why This Is A Control-Plane Decision
The wiring is a config file. The reason to do it is a governance decision that the rest of the industry is converging on from the other direction.
Enterprises no longer run one model. In a16z’s CIO survey, 37% of enterprises now run five or more models in production, and the driver is differentiation by use case, not just lock-in avoidance — one model wins on code completion, another on system design, another purely on cost. That same report notes procurement now looks like traditional software buying, where security and price often outweigh raw accuracy: “for most tasks, all the models perform well enough now — so pricing has become a much more important factor.” Model choice is a business gate, not a preference.
The catch is that choice is getting harder to keep. As agentic workflows mature, a16z finds switching costs are rising: prompts, tools, and guardrails get tuned to one model’s quirks, so “changing models is now a task that can take a lot of engineering time.” The defense against that lock-in is a stable harness over a swappable plane. If the model is a config value behind one API, swapping it is a config change. If the model is wired directly into each tool, swapping it is a migration.
That is why a category has formed around this exact shape. Analysts and vendors now call it the AI control plane or agent gateway — one layer that owns identity, policy, audit, cost attribution, and data residency for every model call, explicitly including terminal-based coding agents. The market is consolidating fast enough that gateways are being acquired and donated to foundations. Most teams reach for a third-party gateway to get this. The point of this post is narrower and cheaper: if you are already on AWS, Bedrock is that plane. One API (Converse), one IAM boundary, one audit surface, inference profiles for capacity and region strategy, and data that stays inside the account. Pointing the coding harness at it means the developer gets the governed boundary without a new vendor in the request path.
Zed’s editor and agent harness are open source (GPL-3.0-or-later, with Apache-2.0 components), from Zed Industries. Zed also offers a Business plan — a per-seat license fee that buys the enterprise governance features on top: org-wide model policies, data-governance controls, role-based access, and unified spend visibility. The fee buys the controls, not the models. Inference still runs through your own Amazon Bedrock endpoint, in your own AWS account. So the layers stack cleanly: pay Zed for the governance layer, keep Bedrock as the model-agnostic plane underneath it.
The Bedrock Configuration
The stable Bedrock version uses Zed’s native Bedrock provider with an AWS profile and region:
{
"agent": {
"default_model": {
"provider": "bedrock",
"model": "global.anthropic.claude-sonnet-5",
"effort": "MEDIUM",
"enable_thinking": true
},
"favorite_models": [
{
"provider": "bedrock",
"model": "global.anthropic.claude-sonnet-5"
},
{
"provider": "bedrock",
"model": "global.anthropic.claude-opus-4-8"
}
]
},
"language_models": {
"bedrock": {
"authentication_method": "named_profile",
"profile": "<aws-profile>",
"region": "us-east-1"
}
}
}
The important choice is the global.* model ID. Amazon Bedrock inference profiles can route model invocation across Regions and are the right fit when the account exposes global profiles for the model family. In the account I checked, the current useful pair was:
global.anthropic.claude-sonnet-5global.anthropic.claude-opus-4-8
Sonnet 5 is the default because it is the latest Anthropic model visible in the account. Opus 4.8 stays as a favorite because some work still wants the premium reasoning path. That is enough for the Anthropic slice. Adding every older model to the picker turns model selection into inventory management.
The CLI Is The Source Of Truth
The UI can lie by omission. Docs can lag. Limited previews can exist before a model appears in every account. The CLI is the first verification layer:
aws bedrock list-foundation-models \
--region us-east-1 \
--query 'modelSummaries[].[providerName,modelId,modelName]' \
--output table
aws bedrock list-inference-profiles \
--region us-east-1 \
--query 'inferenceProfileSummaries[].[inferenceProfileId,inferenceProfileName,status]' \
--output table
For US-only validation, run the same check in us-east-1, us-east-2, and us-west-2. The result is not a universal statement about Bedrock availability. It is the truth for that account at that moment.
That account-specific read matters for OpenAI and Grok. The point of the setup is one free Zed harness across Claude, OpenAI, and xAI frontier models through Bedrock, plus the open-weight catalog. The catch is that the proprietary frontier models do not appear in list-foundation-models or list-inference-profiles at all — those commands cover the native Converse catalog, and OpenAI GPT and Grok live behind the separate OpenAI-shaped endpoint. The CLI check above will not show them even when the account can reach them. The account I checked exposed the full frontier and open-weight set through that endpoint’s own model list, much of which never surfaced in the standard catalog.
That is the trap: the native catalog being empty of these models is not evidence they are unavailable. It is evidence you are looking at the wrong plane.
The OpenAI And Grok Path
OpenAI GPT and xAI Grok models do not come through the native Bedrock Converse API. They come through a separate Bedrock endpoint that speaks the OpenAI API shape: bedrock-mantle.<region>.api.aws. The catalog and inference profiles used by the native provider never surface them, which is why they look absent even when the account can reach them.
The proprietary frontier models are the headline, but the same endpoint is where the open-weight catalog lives too. Bedrock carries a wide set of open-weight models — families like DeepSeek, Qwen, GLM, Kimi, Mistral, Gemma, and Nemotron — and many answer on this OpenAI-shaped endpoint rather than the Converse API. The catalog is broad and refreshes often, though it trails the absolute open-weight frontier by roughly one to three months: the newest top-of-leaderboard open weights usually land on Bedrock a wave or two after their public release. The point holds regardless of which model is on top this month — the harness stays put, the model plane underneath it moves.
The endpoint splits by model family. GPT 5.x answers only on the Responses API (/openai/v1/responses). Grok and the open-weight models answer on Chat Completions (/openai/v1/chat/completions). The path prefix is /openai/v1, not /v1 — the bare /v1/models route lists models but the inference routes live under /openai.
Zed’s openai_compatible provider can represent either shape, but it cannot reach the endpoint directly. Two mismatches block it. First, the endpoint authenticates with an AWS bearer token, not a static API key, and the token is region-scoped and short-lived. Second, GPT 5.x and Grok use different tool-call and message formats, and GPT 5.x’s Responses API streaming events do not match the Chat Completions stream shape Zed expects.
A small local proxy closes both gaps. It mints a bearer token per region from the AWS session, routes each model to the correct rail, translates tool and message formats for the Responses API, and adapts the Responses streaming events back into Chat Completions chunks. Zed points at http://127.0.0.1:<port> as an openai_compatible provider and treats the models as first-class — streaming and tool use included.
{
"language_models": {
"openai_compatible": {
"bedrock-mantle": {
"api_url": "http://127.0.0.1:4327",
"available_models": [
{
"name": "openai.gpt-5.5",
"display_name": "GPT 5.5 (Bedrock)",
"max_tokens": 128000,
"capabilities": {
"tools": true,
"images": false,
"parallel_tool_calls": false,
"prompt_cache_key": false,
"chat_completions": true
}
},
{
"name": "xai.grok-4.3",
"display_name": "Grok 4.3 (Bedrock)",
"max_tokens": 1000000,
"capabilities": {
"tools": true,
"images": false,
"parallel_tool_calls": false,
"prompt_cache_key": false,
"chat_completions": true
}
}
]
}
}
}
}
The proxy carries the model-family logic. Zed sees one endpoint. The right sequence still holds: model visible in the endpoint’s model list, API call succeeds against the correct rail, proxy translates the formats, editor config lands, model becomes a favorite. Reversing that sequence creates a polished model picker full of broken doors.
The Proxy, In Full
The proxy is one Python file with no dependencies beyond aws-bedrock-token-generator. It holds a model table, mints tokens per region, and translates in both directions. Swap the profile name and model IDs for what your account exposes.
#!/usr/bin/env python3
"""Bedrock Mantle proxy for an OpenAI-compatible client (e.g. Zed).
Streaming + tool calls, both rails. pip install aws-bedrock-token-generator
"""
import json, os, sys, time, urllib.error, urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from aws_bedrock_token_generator import provide_token
HOST = os.environ.get("MANTLE_PROXY_HOST", "127.0.0.1")
PORT = int(os.environ.get("MANTLE_PROXY_PORT", "4327"))
AWS_PROFILE = os.environ.get("AWS_PROFILE", "default")
# rail: "openai_responses" (GPT 5.x) or "openai_chat" (Grok, open-weight models)
MODELS = {
"openai.gpt-5.5": {"region": "us-east-1", "rail": "openai_responses"},
"xai.grok-4.3": {"region": "us-west-2", "rail": "openai_chat"},
}
_token_cache = {}
def log(msg): print(f"[proxy] {msg}", file=sys.stderr, flush=True)
def get_token(region):
c = _token_cache.get(region)
if c and c["exp"] > time.time() + 60:
return c["token"]
saved = {k: os.environ.get(k) for k in ("AWS_REGION", "AWS_DEFAULT_REGION", "AWS_PROFILE")}
os.environ.update({"AWS_PROFILE": AWS_PROFILE, "AWS_REGION": region, "AWS_DEFAULT_REGION": region})
try:
token = provide_token()
finally:
for k, v in saved.items():
os.environ.pop(k, None) if v is None else os.environ.__setitem__(k, v)
_token_cache[region] = {"token": token, "exp": time.time() + 3000}
return token
def to_text(content):
if isinstance(content, str): return content
if isinstance(content, list):
return "\n".join(p.get("text", "") for p in content if isinstance(p, dict))
return ""
def build_chat(model, cfg, messages, tools, max_tokens, stream):
url = f"https://bedrock-mantle.{cfg['region']}.api.aws/openai/v1/chat/completions"
body = {"model": model, "messages": messages,
"max_completion_tokens": max_tokens, "stream": stream}
if tools:
body["tools"] = tools
body["tool_choice"] = "auto"
return url, body
def build_responses(model, cfg, messages, tools, max_tokens, stream):
url = f"https://bedrock-mantle.{cfg['region']}.api.aws/openai/v1/responses"
system, items = None, []
for m in messages:
role = m.get("role")
if role == "system":
system = to_text(m.get("content", "")); continue
if role == "tool":
items.append({"type": "function_call_output",
"call_id": m.get("tool_call_id", ""),
"output": to_text(m.get("content", ""))}); continue
if role == "assistant" and m.get("tool_calls"):
for tc in m["tool_calls"]:
fn = tc.get("function", {})
items.append({"type": "function_call", "call_id": tc.get("id", ""),
"name": fn.get("name", ""), "arguments": fn.get("arguments", "{}")})
t = to_text(m.get("content", ""))
if t: items.append({"role": "assistant", "content": t})
continue
ttype = "input_text" if role == "user" else "output_text"
items.append({"role": role,
"content": [{"type": ttype, "text": to_text(m.get("content", ""))}]})
body = {"model": model, "input": items, "max_output_tokens": max_tokens,
"stream": stream, "store": False}
if system: body["instructions"] = system
if tools:
body["tools"] = [{"type": "function", "name": t["function"]["name"],
"description": t["function"].get("description", ""),
"parameters": t["function"].get("parameters", {})} for t in tools]
body["tool_choice"] = "auto"
return url, body
def upstream(url, body, token):
return urllib.request.Request(url, data=json.dumps(body).encode(),
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json",
"Accept": "text/event-stream" if body.get("stream") else "application/json"},
method="POST")
def responses_stream_to_chat(resp):
mid, created, tools_seen = f"c-{int(time.time()*1000)}", int(time.time()), False
for raw in resp:
line = raw.decode("utf-8", "replace").rstrip()
if not line.startswith("data: "): continue
data = line[6:]
if data == "[DONE]":
yield b"data: [DONE]\n\n"; return
try: ev = json.loads(data)
except json.JSONDecodeError: continue
t = ev.get("type", "")
def chunk(delta, finish=None):
return ("data: " + json.dumps({"id": mid, "object": "chat.completion.chunk",
"created": created, "model": ev.get("model", ""),
"choices": [{"index": 0, "delta": delta, "finish_reason": finish}]}) + "\n\n").encode()
if t == "response.output_text.delta":
yield chunk({"content": ev.get("delta", "")})
elif t == "response.output_item.added" and ev.get("item", {}).get("type") == "function_call":
tools_seen = True; it = ev["item"]; idx = ev.get("output_index", 0)
yield chunk({"tool_calls": [{"index": idx, "id": it.get("id", ""), "type": "function",
"function": {"name": it.get("name", ""), "arguments": ""}}]})
elif t == "response.function_call_arguments.delta":
yield chunk({"tool_calls": [{"index": ev.get("output_index", 0),
"function": {"arguments": ev.get("delta", "")}}]})
elif t == "response.completed":
yield chunk({}, "tool_calls" if tools_seen else "stop")
yield b"data: [DONE]\n\n"; return
def responses_json_to_chat(payload, model):
text, tools = [], []
for i, item in enumerate(payload.get("output", [])):
if item.get("type") == "message":
text += [c.get("text", "") for c in item.get("content", []) if c.get("type") == "output_text"]
elif item.get("type") == "function_call":
tools.append({"id": item.get("id", f"call_{i}"), "type": "function",
"function": {"name": item.get("name", ""), "arguments": item.get("arguments", "{}")}})
msg = {"role": "assistant", "content": "".join(text) or (None if tools else "")}
if tools: msg["tool_calls"] = tools
return {"id": f"c-{int(time.time()*1000)}", "object": "chat.completion",
"created": int(time.time()), "model": model,
"choices": [{"index": 0, "message": msg,
"finish_reason": "tool_calls" if tools else "stop"}]}
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *a): pass
def _json(self, status, payload):
d = json.dumps(payload).encode()
self.send_response(status); self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(d))); self.end_headers(); self.wfile.write(d)
def do_GET(self):
if self.path in ("/v1/models", "/models"):
self._json(200, {"object": "list", "data": [{"id": k, "object": "model",
"created": 0, "owned_by": "bedrock-mantle"} for k in MODELS]})
else: self._json(404, {"error": {"message": "not found"}})
def do_POST(self):
if self.path not in ("/v1/chat/completions", "/chat/completions"):
return self._json(404, {"error": {"message": "not found"}})
req = json.loads(self.rfile.read(int(self.headers.get("Content-Length", "0"))))
model = req.get("model", "")
if model not in MODELS:
return self._json(400, {"error": {"message": f"unsupported model: {model}"}})
cfg = MODELS[model]
messages, tools = req.get("messages", []), req.get("tools") or []
max_tokens = req.get("max_completion_tokens") or req.get("max_tokens") or 4096
stream = bool(req.get("stream", False))
token = get_token(cfg["region"])
builder = build_responses if cfg["rail"] == "openai_responses" else build_chat
url, body = builder(model, cfg, messages, tools, max_tokens, stream)
try:
if stream:
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "close"); self.end_headers()
with urllib.request.urlopen(upstream(url, body, token), timeout=300) as up:
gen = responses_stream_to_chat(up) if cfg["rail"] == "openai_responses" else up
for line in gen:
self.wfile.write(line); self.wfile.flush()
else:
with urllib.request.urlopen(upstream(url, body, token), timeout=120) as up:
payload = json.loads(up.read())
result = responses_json_to_chat(payload, model) if cfg["rail"] == "openai_responses" else payload
self._json(200, result)
except urllib.error.HTTPError as e:
self._json(e.code, {"error": {"message": e.read().decode("utf-8", "replace")}})
if __name__ == "__main__":
log(f"listening on http://{HOST}:{PORT}/v1 models: {', '.join(MODELS)}")
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
Start it with an AWS profile that can call Bedrock, then point Zed’s openai_compatible provider at the port. Two details are load-bearing. The stream uses Connection: close, not Transfer-Encoding: chunked — a plain HTTPServer does not chunk-encode, and a client expecting chunked framing fails with a decode error. And the token must be minted for the same region as the model’s endpoint, or the call returns a region-scope error.
What Free Does Not Mean
The Zed harness can be free as the editor-side agent surface. Bedrock inference is still metered. Frontier models are still frontier-model economics. The win is that the harness is not another model subscription gate.
The other missing piece is automatic model routing inside the harness.
Some coding agents use a premium model for the visible reasoning path and a faster smaller model for internal classification, summarization, sub-agent work, or cheap tool-adjacent tasks. That is not merely a model setting. It is harness behavior.
Zed’s native agent model setting does not appear to add that internal routing layer. If the selected model is Sonnet 5, assume the native Zed Agent is using Sonnet 5 for the agent session unless Zed documents otherwise. If an ACP-backed agent does internal routing, that behavior belongs to the external agent process, not to Zed.
This is the trade:
| Choice | Benefit | Cost |
|---|---|---|
| Zed native Agent on Bedrock | Free editor harness across reachable Bedrock models | Bedrock usage is still billed; internal routing is less visible |
| Mature CLI agent directly | Full native harness behavior | Separate surface from the editor |
| Mature CLI agent through ACP | Better editor integration | Protocol layer may hide or flatten parts of the native experience |
The wrong expectation is “same model, same behavior.” The better expectation is “same model plane, different harness.”
So What
Wiring Zed to Bedrock is worth doing because it turns Zed’s agent harness into a portable surface across the models the account can reach — and it does it by riding a boundary the enterprise already trusts. The coding agent inherits one auth boundary, one audit surface, and one place inference is metered. Model choice becomes a config change instead of a per-tool migration. Region and capacity strategy — inference profiles for cross-region failover, provisioned throughput for guaranteed capacity — is inherited, not rebuilt per developer. And there is no third-party gateway added to the path to get any of it. For a regulated org distributing one inference budget across many teams and use cases, that is the difference between governed model access and a fleet of ungoverned direct integrations.
But the configuration is not the agent architecture. The real architecture has three layers:
- Model plane: Bedrock native Converse provider, global inference profiles, and the separate Mantle endpoint for proprietary frontier models and open-weight models
- Editor plane: Zed Agent, Agent Panel, tools, and model picker
- Harness plane: native Zed behavior, a local translation proxy for non-Converse models, or an external ACP/CLI agent’s own routing logic
Keep those layers separate and the setup stays legible. Collapse them and every failure becomes confusing: is the model unavailable, the provider misconfigured, the account not allowlisted, the proxy translating a format wrong, the ACP bridge flattening state, or the harness choosing a different internal path?
The open thread: the OpenAI and Grok path runs through a local translation proxy, not native Zed support. I have an open issue with the Zed team asking for native Mantle support in the Bedrock provider. Until that lands, this is a working workaround, not a clean integration.