Skip to content
Go back

One Coding Agent, Three Bedrock APIs: Wiring OpenCode to Claude, GPT, Grok, and Kimi

One model ID is not one integration contract. I wired ten frontier model profiles into OpenCode through Amazon Bedrock Runtime, and the setup only held when each family used the API it actually speaks.

Claude uses Messages. OpenAI GPT uses Responses. Grok and Kimi use Chat Completions. OpenCode stays the coding harness across all three, while the provider package, wire format, and model profile change underneath it.

TL;DR

The architecture

OpenCode custom providers separate the coding harness from the model client. The provider package converts OpenCode’s internal message and tool representation into the target API.

That creates a clean three-rail architecture:

OpenCode
  |
  +-- @ai-sdk/anthropic
  |     `-- /anthropic/v1/messages
  |           `-- Claude
  |
  +-- @ai-sdk/openai
  |     `-- /openai/v1/responses
  |           `-- GPT
  |
  `-- @ai-sdk/openai-compatible
        `-- /openai/v1/chat/completions
              +-- Grok
              `-- Kimi

The split matters during agent loops. A text-only smoke test can pass through a generic compatibility layer while tool results, reasoning blocks, images, or stream termination fail on the second turn. OpenCode is still tracking native package selection across Chat, Responses, Messages, and Converse, so explicit provider packages remain the clearest configuration boundary.

The credential bridge

The provider packages expect API-key-shaped authentication. My source of authority is a named AWS profile resolved through the standard AWS credential provider chain, not a static model key.

The bridge exists because of that client boundary, not because Bedrock requires a proxy. OpenCode’s built-in @ai-sdk/amazon-bedrock provider can read an AWS profile and sign requests, but it selects Converse. The native @ai-sdk/anthropic, @ai-sdk/openai, and @ai-sdk/openai-compatible packages select the required APIs but cannot receive a signing function through OpenCode’s JSON configuration.

A local bridge on 127.0.0.1:8769 closes that gap. It loads the named profile, resolves current temporary credentials, and applies AWS Signature Version 4 to the complete HTTP request immediately before forwarding it. Credential renewal stays inside the standard AWS provider chain; there is no separate bearer-token cache or refresh loop.

If the profile’s underlying login expires and requires human sign-in, the credential chain cannot manufacture a new authenticated session. The operational check is simple: if aws sts get-caller-identity --profile <aws-profile> succeeds, the bridge can sign Bedrock requests with that profile.

The bridge has three rules:

Incoming pathUpstream pathAuthentication
/anthropic/v1/messagesSameSigV4, service bedrock
/openai/v1/responsesSameSigV4, service bedrock
/openai/v1/chat/completionsSameSigV4, service bedrock

The process binds only to loopback, signs every upstream attempt, retries service failures before visible output, and never replays a request after streaming output begins. It logs request shape rather than prompt content. Its environment contains the machine-specific values:

BEDROCK_AWS_PROFILE=<aws-profile>
BEDROCK_AWS_REGION=us-east-1
BEDROCK_UPSTREAM_BASE_URL=https://bedrock-runtime.us-east-1.amazonaws.com
BEDROCK_ADAPTER_HOST=127.0.0.1
BEDROCK_ADAPTER_PORT=8769

The signing core is small because the AWS profile already carries the credential logic:

import os

from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.session import Session

region = os.environ["BEDROCK_AWS_REGION"]
profile = os.environ.get("BEDROCK_AWS_PROFILE") or None
session = Session(profile=profile)

def signed_headers(url, body, headers):
    credentials = session.get_credentials().get_frozen_credentials()
    request = AWSRequest(method="POST", url=url, data=body, headers=headers)
    SigV4Auth(credentials, "bedrock", region).add_auth(request)
    return dict(request.headers.items())

The OpenCode config retains a non-secret placeholder because the provider packages require an apiKey field. The bridge discards that value and replaces the incoming authentication with SigV4 headers. Two alternatives remove the bridge: a custom fetch layer that signs these native requests inside OpenCode, or the built-in profile-aware provider using Converse. Neither currently preserves both the native API split and the refreshable named-profile setup without adding equivalent signing logic somewhere else.

The OpenCode configuration

This is the public-safe core of the working opencode.jsonc. It contains the ten tested model profiles and the model-specific reasoning choices. Add current price metadata separately if accurate local cost estimates matter; prices change more often than the protocol contract.

{
  "$schema": "https://opencode.ai/config.json",
  "model": "bedrock-messages/global.anthropic.claude-sonnet-5",
  "provider": {
    "bedrock-messages": {
      "npm": "@ai-sdk/anthropic",
      "name": "Bedrock Runtime — Claude Messages",
      "options": {
        "baseURL": "http://127.0.0.1:8769/anthropic/v1",
        "apiKey": "local-bedrock-runtime-adapter"
      },
      "models": {
        "global.anthropic.claude-sonnet-5": {
          "name": "Claude Sonnet 5",
          "family": "claude-sonnet",
          "reasoning": true,
          "temperature": false,
          "attachment": true,
          "tool_call": true,
          "limit": { "context": 1000000, "output": 128000 },
          "modalities": {
            "input": ["text", "image", "pdf"],
            "output": ["text"]
          }
        },
        "global.anthropic.claude-opus-5": {
          "name": "Claude Opus 5",
          "family": "claude-opus",
          "reasoning": true,
          "temperature": false,
          "attachment": true,
          "tool_call": true,
          "limit": { "context": 1000000, "output": 128000 },
          "modalities": {
            "input": ["text", "image", "pdf"],
            "output": ["text"]
          }
        },
        "global.anthropic.claude-haiku-4-5-20251001-v1:0": {
          "name": "Claude Haiku 4.5",
          "family": "claude-haiku",
          "reasoning": true,
          "temperature": true,
          "attachment": true,
          "tool_call": true,
          "limit": { "context": 200000, "output": 64000 },
          "modalities": {
            "input": ["text", "image", "pdf"],
            "output": ["text"]
          }
        },
        "global.anthropic.claude-fable-5-1": {
          "name": "Claude Fable 5.1",
          "family": "claude-fable",
          "reasoning": true,
          "temperature": false,
          "attachment": true,
          "tool_call": true,
          "limit": { "context": 1000000, "output": 128000 },
          "modalities": {
            "input": ["text", "image", "pdf"],
            "output": ["text"]
          }
        }
      }
    },

    "bedrock-responses": {
      "npm": "@ai-sdk/openai",
      "name": "Bedrock Runtime — OpenAI Responses",
      "options": {
        "baseURL": "http://127.0.0.1:8769/openai/v1",
        "apiKey": "local-bedrock-runtime-adapter"
      },
      "models": {
        "global.openai.gpt-6-astra": {
          "name": "GPT-6 Astra",
          "family": "gpt-astra",
          "reasoning": true,
          "temperature": false,
          "attachment": true,
          "tool_call": true,
          "limit": {
            "context": 1050000,
            "input": 922000,
            "output": 128000
          },
          "modalities": {
            "input": ["text", "image"],
            "output": ["text"]
          },
          "variants": {
            "xhigh": { "reasoningEffort": "xhigh" },
            "max": { "reasoningEffort": "max" }
          }
        },
        "global.openai.gpt-5.6-sol": {
          "name": "GPT-5.6 Sol",
          "family": "gpt-sol",
          "reasoning": true,
          "temperature": false,
          "attachment": true,
          "tool_call": true,
          "limit": {
            "context": 1000000,
            "input": 872000,
            "output": 128000
          },
          "modalities": {
            "input": ["text", "image"],
            "output": ["text"]
          },
          "variants": {
            "none": { "reasoningEffort": "none" },
            "xhigh": { "reasoningEffort": "xhigh" },
            "max": { "reasoningEffort": "max" }
          }
        },
        "global.openai.gpt-5.6-terra": {
          "name": "GPT-5.6 Terra",
          "family": "gpt-terra",
          "reasoning": true,
          "temperature": false,
          "attachment": true,
          "tool_call": true,
          "limit": {
            "context": 1000000,
            "input": 872000,
            "output": 128000
          },
          "modalities": {
            "input": ["text", "image"],
            "output": ["text"]
          },
          "variants": {
            "none": { "reasoningEffort": "none" },
            "xhigh": { "reasoningEffort": "xhigh" },
            "max": { "reasoningEffort": "max" }
          }
        },
        "global.openai.gpt-5.6-luna": {
          "name": "GPT-5.6 Luna",
          "family": "gpt-luna",
          "reasoning": true,
          "temperature": false,
          "attachment": true,
          "tool_call": true,
          "limit": {
            "context": 1000000,
            "input": 872000,
            "output": 128000
          },
          "modalities": {
            "input": ["text", "image"],
            "output": ["text"]
          },
          "variants": {
            "none": { "reasoningEffort": "none" },
            "xhigh": { "reasoningEffort": "xhigh" },
            "max": { "reasoningEffort": "max" }
          }
        }
      }
    },

    "bedrock-chat": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Bedrock Runtime — Chat Completions",
      "options": {
        "baseURL": "http://127.0.0.1:8769/openai/v1",
        "apiKey": "local-bedrock-runtime-adapter",
        "includeUsage": true
      },
      "models": {
        "global.xai.grok-4.6": {
          "name": "Grok 4.6",
          "family": "grok",
          "reasoning": true,
          "temperature": false,
          "attachment": true,
          "tool_call": true,
          "limit": {
            "context": 500000,
            "input": 468000,
            "output": 32000
          },
          "modalities": {
            "input": ["text", "image"],
            "output": ["text"]
          },
          "variants": {
            "xhigh": { "reasoningEffort": "xhigh" }
          }
        },
        "global.moonshotai.kimi-k3": {
          "name": "Kimi K3",
          "family": "kimi",
          "reasoning": true,
          "temperature": false,
          "attachment": true,
          "tool_call": true,
          "limit": {
            "context": 1000000,
            "input": 968000,
            "output": 32000
          },
          "modalities": {
            "input": ["text", "image"],
            "output": ["text"]
          },
          "variants": {
            "none": { "reasoningEffort": "none" },
            "low": { "reasoningEffort": "low" },
            "medium": { "reasoningEffort": "medium" },
            "high": { "reasoningEffort": "high" },
            "xhigh": { "reasoningEffort": "xhigh" }
          }
        }
      }
    }
  }
}

OpenCode generates the normal low, medium, and high variants for compatible reasoning models. The explicit entries above add the edge choices that were missing from the generated model metadata.

What the model cards say

The context window belongs to the model. The input reserve belongs to the client. I keep both visible so OpenCode does not fill the entire window before leaving room for reasoning and output.

Model familyPublished contextConfigured output reserveReasoning choices exposed
Claude Sonnet 5, Opus 5, Fable 5.11M128Klow through max
Claude Haiku 4.5200K64Khigh, max
GPT-5.6 Sol, Terra, Luna1M128Knone through max
GPT-6 Astra1.05M128Klow through max
Grok 4.6500K32K client caplow through xhigh
Kimi K31M32K client capnone through xhigh

The Claude limits come from the current Sonnet 5, Opus 5, Haiku 4.5, and Fable 5.1 cards. The OpenAI values come from the GPT-6 Astra, GPT-5.6 Sol, GPT-5.6 Terra, and GPT-5.6 Luna cards.

Grok 4.6 publishes a 500K context window but no separate maximum output. Kimi K3 publishes 1M context and recommends Chat Completions, but also omits a maximum output value. The 32K values above are tested OpenCode client caps, not claims about each model’s full generation ceiling.

The measured result

I tested the service boundary before testing the harness.

Direct Bedrock Runtime calls covered all ten model profiles:

The OpenCode layer then ran the same ten models headlessly. All ten completed through the intended provider. Claude used Messages, GPT used Responses, and Grok/Kimi used Chat Completions. Representative tool loops also completed across all four families.

The deterministic local suite contains 16 tests and 27 model-specific subtests for path routing, SigV4 authentication, safe retry boundaries, model limits, modalities, prices, and resolved reasoning variants.

opencode run \
  --dir /tmp \
  --pure \
  --model bedrock-responses/global.openai.gpt-5.6-terra \
  --variant medium \
  "Reply with exactly HEADLESS_OK"

The model picker is not the proof. The proof is a request on the expected path, a response from the intended profile, a working tool-result turn, and usage metadata that matches the selected reasoning mode.

What remains open

I did not send a full 500K or 1M prompt to every model. The context ceilings are published model-card values; the direct probes validated output parameters, images, tools, reasoning settings, and API compatibility. A boundary load test would be a separate, materially billed experiment.

Grok and Kimi also receive a reduced core-tool set in my bridge. OpenCode has an open Kimi tool-schema projection issue: one incompatible MCP schema can reject the entire request before the model chooses a tool. Claude and GPT receive the full tool list because their native providers accepted it in the headless tests.

OpenCode is the stable harness here. The custom part is the SigV4-signing loopback bridge. The open question is whether OpenCode will make that bridge unnecessary by combining native Messages, Responses, and Chat Completions selection with refreshable AWS profile authentication. Until then, the API rail is part of the model configuration—and treating it as metadata is how a working model becomes a broken agent.


Share this post on:


Previous Post
Where AgentCore Sits in the Architecture
Next Post
Jev Does Not Write. That Is the Point.