DEV Community

Amit
Amit

Posted on Originally published at artificialcuriositylabs.ai

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

  • OpenCode uses Vercel AI SDK provider packages for model transport, so the npm field determines more than a dependency: it selects the request and streaming protocol.
  • Three provider blocks preserve the native model contracts: @ai-sdk/anthropic, @ai-sdk/openai, and @ai-sdk/openai-compatible.
  • A loopback credential bridge turns a refreshable AWS profile into the API headers these packages expect without storing a Bedrock token in OpenCode.
  • Ten direct Bedrock calls and ten OpenCode headless runs passed, including image input, function tools, output limits, and selectable reasoning effort.

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
Enter fullscreen mode Exit fullscreen mode

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 whose credential process returns short-lived STS role credentials, not a static 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, but it selects Converse. The native @ai-sdk/anthropic, @ai-sdk/openai, and @ai-sdk/openai-compatible packages select the required APIs but do not turn an AWS profile into request authentication.

A local bridge on 127.0.0.1:8769 closes that gap. The AWS credential chain refreshes the profile's STS credentials. The bridge then calls a token helper with that profile, mints a short-lived Bedrock token through the public aws-bedrock-token-generator package, caches it for 40 minutes, and replaces the placeholder client credential before forwarding the request.

Those are separate refresh loops. The profile owns STS refresh; the bridge owns Bedrock-token refresh. If the upstream login reaches a point that requires interactive authentication, neither loop can manufacture a new session.

The bridge has three rules:

Incoming path Upstream path Authentication header
/anthropic/v1/messages Same x-api-key: <short-lived-token>
/openai/v1/responses Same Authorization: Bearer <short-lived-token>
/openai/v1/chat/completions Same Authorization: Bearer <short-lived-token>

The process binds only to loopback, refreshes after an authentication rejection, retries service failures before visible output, and 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
BEDROCK_TOKEN_HELPER=<path-to-token-helper>
Enter fullscreen mode Exit fullscreen mode

The helper itself is small because the AWS profile already carries the credential logic:

#!/usr/bin/env python3
import argparse
import json
import os

from aws_bedrock_token_generator import provide_token

parser = argparse.ArgumentParser()
parser.add_argument("--profile", required=True)
parser.add_argument("--region", required=True)
args = parser.parse_args()

os.environ["AWS_PROFILE"] = args.profile
os.environ["AWS_REGION"] = args.region
os.environ["AWS_DEFAULT_REGION"] = args.region

print(json.dumps({"access_token": provide_token()}))
Enter fullscreen mode Exit fullscreen mode

The OpenCode config uses a non-secret placeholder value because the bridge discards it. Three alternatives remove the bridge: a long-lived Bedrock API key, a custom fetch layer that signs these native HTTP requests with SigV4, or the built-in profile-aware provider using Converse. None preserves both the native API split and the refreshable named-profile setup without adding equivalent authentication 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" }
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 family Published context Configured output reserve Reasoning choices exposed
Claude Sonnet 5, Opus 5, Fable 5.1 1M 128K low through max
Claude Haiku 4.5 200K 64K high, max
GPT-5.6 Sol, Terra, Luna 1M 128K none through max
GPT-6 Astra 1.05M 128K low through max
Grok 4.6 500K 32K client cap low through xhigh
Kimi K3 1M 32K client cap none 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:

  • Text generation returned the requested value.
  • Every model accepted its configured output-token parameter.
  • Every model accepted a generated 64×64 PNG.
  • Every model produced a forced function call. Fable rejected forced tool or any selection but produced the same call with auto, which matches OpenCode's request.
  • Kimi accepted none, low, medium, high, and xhigh; none reported zero reasoning tokens.

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 15 tests for path routing, authentication headers, retries, 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"
Enter fullscreen mode Exit fullscreen mode

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 short-lived credential 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.

Top comments (0)