본문 바로가기
MegazoneCloud
← 전체 글
AWS

AWS Bedrock 서울 리전에서 Grok 4.6 도입 전에 확인할 것들

서울에서 Grok 4.6은 GPT-5.6과 동일한 위치 제약(global.* 전용, 해외 처리)을 갖지만 사용 조건은 다르므로, 도입 판단은 같은 기준으로 할 수 없다

목차

  1. Prompt Caching & Effort
  2. Guardrails
  3. API Path
  4. 문서와 실제
  5. Framework
  6. 도입 판단
  7. 부록: 재현 환경
  8. 참고 링크

2026년 8월 20일 새벽 2시, 서울 리전에서 Grok 4.6을 부를 수 있게 됐습니다. 모델 카드에 적힌 출시일은 8월 18일인데, 계정에서 조회한 startOfLifeTime은 2026-08-19T17:00:00Z입니다. 한국 시각으로 옮기면 8월 20일 새벽입니다. 이 글의 수치는 그렇게 열린 지 얼마 안 된 모델을 잰 값입니다. 문서와 실제가 갈리는 부분도, 프레임워크가 아직 못 따라온 부분도 그대로 적었습니다. 앞으로 개선될 부분이라는 점을 감안한, 지금 시점의 기록입니다.

결론부터 적겠습니다. 서울에서 쓸 수 있는 프로파일은 global.xai.grok-4.6 하나뿐입니다. GPT-5.6 때와 같은 제약입니다. 사용 가능한 리전만 놓고 보면 두 모델은 비슷한 포지션에 있습니다. 하지만 앞 글에서 정리한 설정을 그대로 옮기면 어긋납니다. 캐시 경계를 잡는 cachePoint는 거절당하고, Guardrails는 아예 붙지 않습니다. 분류 작업의 기본값으로 삼았던 effort=none은 어려운 문제에서 1,000요청 기준 $4.35짜리 작업을 $25.28로 만들었습니다. (2026년 8월 24일 확인)

서울 리전의 프로파일 목록부터 확인하겠습니다.

$ aws bedrock list-foundation-models --region ap-northeast-2 \
    --query "modelSummaries[?contains(modelId,'grok')].[modelId,modelName,providerName]" \
    --output text
xai.grok-4.6    Grok 4.6    xAI

$ aws bedrock list-inference-profiles --region ap-northeast-2 \
    --query "inferenceProfileSummaries[?contains(inferenceProfileId,'grok')].[inferenceProfileId,status]" \
    --output text
global.xai.grok-4.6     ACTIVE

실제 호출 예제는 이렇습니다. 프로파일 ID를 그대로 modelId에 넣으면 됩니다.

import boto3

br = boto3.client("bedrock-runtime", region_name="ap-northeast-2")
r = br.converse(
    modelId="global.xai.grok-4.6",
    messages=[{"role": "user", "content": [{"text": "9.11과 9.9 중 뭐가 큰가? 한 문장."}]}],
    inferenceConfig={"maxTokens": 512},
)

# 응답 블록이 둘입니다 — 추론 블록과 본문. text 블록만 골라야 합니다.
text = "".join(b["text"] for b in r["output"]["message"]["content"] if "text" in b)
print(text)          # 9.9가 9.11보다 큽니다.

content[0]["text"]로 꺼내면 터집니다. 추론 블록이 앞에 오기 때문인데, GPT-5.6도 같았습니다.

서울 리전 클라이언트에서 다른 ID로 부르면 어떻게 반응하는지도 확인해봤습니다.

호출한 ID에러
xai.grok-4.6Invocation of model ID xai.grok-4.6 with on-demand throughput isn't supported.
us.xai.grok-4.6The provided model identifier is invalid.
apac.xai.grok-4.6The provided model identifier is invalid.
xai.grok-4.3 (이전 모델)The provided model identifier is invalid.

1.Prompt Caching & Effort

다들 아시다시피 프롬프트 캐싱은 연산을 마친 입력 프롬프트를 캐시에 두었다가 같은 내용이 다시 들어오면 재사용하는 최적화입니다. 그래서 멀티턴이 길어지는 에이전트에서 입력 단가를 크게 낮춰줍니다. 앞 글에서 비용을 낮추는 방법으로 정리한 것이 이 캐싱과 effort=none 두 가지였는데, Grok 4.6에서는 둘 다 그대로 옮겨지지 않습니다. 하나는 경계를 잡을 수 없고, 다른 하나는 조건에 따라 손해가 됩니다.

캐시 경계는 지정할 수 없습니다

캐싱부터 보겠습니다. Converse에는 cachePoint로 캐시 경계를 잡는 방식이 있습니다. Claude 계열이 쓰는 방법인데, Grok에서는 거절됩니다.

# 시스템 프롬프트 끝에 캐시 경계를 두고 호출
br.converse(
    modelId="global.xai.grok-4.6",
    system=[{"text": 긴_시스템_프롬프트}, {"cachePoint": {"type": "default"}}],
    messages=[{"role": "user", "content": [{"text": "한 줄 요약"}]}],
    inferenceConfig={"maxTokens": 64},
)
# AccessDeniedException: You invoked an unsupported model or your request
#   did not allow prompt caching. See the documentation for more information.

OpenAI 호환 경로에도 캐시 파라미터가 있는데, 여기서도 거절됩니다.

POST /openai/v1/responses
{
  "model": "global.xai.grok-4.6",
  "input": [{"type": "message", "role": "developer",
             "content": [{"type": "input_text", "text": "(긴 시스템 프롬프트)",
                          "prompt_cache_breakpoint": {"mode": "explicit"}}]}],
  "max_output_tokens": 64
}
→ 400  prompt_cache_breakpoint is not supported with the 'global.xai.grok-4.6' model.

POST /openai/v1/responses
{
  "model": "global.xai.grok-4.6",
  "prompt_cache_options": {"mode": "explicit"},
  "input": [ ... ]
}
→ 400  prompt_cache_options is not supported on this model

Grok만 그런 건 아닙니다. 모델마다 경계를 잡는 자리가 다르고, 서울에서 부를 수 있는 세 모델이 각각 다릅니다.

모델Converse cachePointResponses prompt_cache_breakpoint
Claude Sonnet 4.6동작 (write 10,818)404 — Responses 경로가 없음
GPT-5.6 TerraAccessDenied동작
Grok 4.6AccessDenied400 — 파라미터 미지원

Claude는 Converse, GPT-5.6은 Responses, Grok은 어느 쪽도 아닙니다.

이유는 xAI 문서에 있습니다. xAI의 캐싱은 메시지 배열 앞에서부터 자동으로 걸리고, 경계를 지정하는 개념이 아예 없습니다. 그러면서 “Prompt caching is not 100% guaranteed”라고 못 박아뒀습니다. 메모리 압박으로 캐시가 지워질 수 있고, 요청이 다른 서버로 갈 수도 있다는 것입니다.

그래서 캐싱이 없는 건 아닙니다. 지정하지 않아도 알아서 걸립니다. 7,850토큰짜리 시스템 프롬프트를 네 번 연속 보냈을 때는 이랬습니다.

1회  input=7,850  cacheRead=0
2회  input=7,850  cacheRead=0
3회  input=42     cacheRead=7,808   ← 여기
4회  input=7,850  cacheRead=0

12회 모두 us-west-2로 갔는데도 적중이 네 번뿐이었던 것도 이걸로 설명됩니다. 리전이 같아도 서버가 다르면 캐시가 없습니다.

그럼 적중률을 높일 수는 있을까요. xAI는 x-grok-conv-id 헤더로 같은 서버에 요청을 몰아주라고 안내합니다. 이것까지 포함해 캐싱을 건드릴 수 있는 방법을 하나씩 넣어봤습니다.

넣어본 것결과
x-grok-conv-id 헤더200 — 적중률 4회 중 1회
Chat Completions prompt_cache_key200 — 적중률 4회 중 1회
아무것도 안 넣음200 — 적중률 4회 중 2회

에러가 나지 않고, 아무 일도 일어나지 않습니다. 앞의 거절보다 이쪽이 더 성가십니다. 넣어두고 캐싱을 제어하고 있다고 믿기 쉽습니다.

켜는 것도, 끄는 것도, 경계를 정하는 것도 안 됩니다. cacheWriteInputTokens가 계속 0이라 언제 써지는지도 응답으로는 알 수 없습니다. 긴 프롬프트를 재사용해 입력 단가를 낮추는 설계라면 걸리면 이득, 안 걸려도 그만으로 잡아야 합니다.

effort는 난이도에 따라 값이 뒤집힙니다

비용을 깎을 다음 후보는 effort입니다. 질문의 난이도에 맞춰 추론량을 조절하는 값이고, Converse에서는 additionalModelRequestFields에 {"reasoning": {"effort": "medium"}} 형태로 넘깁니다. 필드 이름이 틀리면 에러 없이 조용히 무시되므로 오타를 확인해야 합니다.

def call(fields):
    r = br.converse(
        modelId="global.xai.grok-4.6",
        messages=[{"role": "user", "content": [{"text": "농장에 닭과 소. 머리 30, 다리 74. 닭은 몇 마리? 숫자만."}]}],
        inferenceConfig={"maxTokens": 2048},
        additionalModelRequestFields=fields,
    )
    return r["usage"]["outputTokens"]

call({"reasoning": {"effort": "none"}})   # 7    ← 먹었습니다
call({"reasoning_effort": "none"})        # 221  ← 무시됐습니다

출력 토큰을 보기 전에는 구분할 방법이 없습니다. 값이 안 먹은 채로 비용을 재면 결론이 통째로 틀어집니다.

두 번째인 reasoning.effort는 조금 더 복잡합니다. 값을 바꿔가며 재봤습니다. 난이도가 다른 문제 네 개에 다섯 단계를 걸고 각각 세 번씩, 모두 60회를 호출했습니다. 아래는 출력 토큰의 3회 중앙값이고, 비용은 Global CRIS 단가로 1,000요청을 환산한 값입니다.

문제nonelowmediumhighxhigh
E — 감정 분류$0.19$2.11$3.03$4.39$4.14
M — 연립방정식$0.17$1.58$1.68$2.04$1.89
H — 평면기하$21.85$3.59$9.07$13.95$6.80
VH — 조합 계수$25.28$4.35$8.69$11.50$10.82

행마다 최저가가 옮겨 다닙니다. 쉬운 두 문제에서 none은 7토큰에 $0.2 안팎으로 압도적입니다. 어려운 두 문제에서는 같은 none이 가장 비쌉니다. VH에서 $25.28인데, 이건 최저인 low의 여섯 배 가까이 됩니다. 지연도 같이 갑니다. VH에서 none이 45.7초, low가 7.9초였습니다. 추론을 끄는 스위치가 어려운 문제에서는 가장 비싼 선택지가 됩니다.

이유는 응답 구조에 있습니다. none을 걸면 reasoningContent 블록이 사라지고 응답 블록이 하나만 옵니다. 추론이 없어지는 게 아니라 본문으로 자리를 옮깁니다. OpenAI 호환 경로로 부르면 usage가 둘을 갈라 보여줍니다. H 문제를 none과 low로 한 번씩 불러 이렇게 나왔습니다.

effort=none   출력 6,091토큰   추론 0    + 본문 6,091
effort=low    출력   582토큰   추론 567  + 본문 15

이 회차에서는 low가 열 배 쌌습니다. 추론 채널이 567토큰을 쓰고 답 한 줄로 끝내는 일을, none은 답변 자리에서 6,091토큰을 쓰고도 못 끝냅니다. low가 내놓은 답은 \(12\sqrt{3}\) 열네 글자가 전부입니다. none의 본문 끝은 이렇습니다.

… Anyway we have S=12√3.

Yes.

This is the solution.

I output this.

정답을 이미 구해놓고 출력하겠다는 말만 반복합니다. 추론 채널이 맡던 “정리하고 끊기”를 본문이 못 합니다. H 문제에서 none은 영어로 검산을 반복하다가 So all good. 같은 문장으로 끝났습니다. 최종 답 형식이 나오지 않습니다. 세 번 중 한 번은 maxTokens 16,384를 전부 태우고 stopReason: max_tokens로 잘렸습니다. 반대로 VH에서는 7토큰 만에 72라고 답한 회차가 있었습니다. 정답은 70이고, 0.93초 만에 끝났습니다. 관측된 실패는 헤매는 쪽과 즉답으로 틀리는 쪽, 두 가지였습니다.

정답은 이렇게 나왔습니다. 3회 중 맞힌 횟수입니다.

문제nonelowmediumhighxhigh
E — 감정 분류33333
M — 연립방정식33333
H — 평면기하3 (맞히긴 하나 끊질 못함)3333
VH — 조합 계수23333

low 이상은 네 문제 모두 3회씩 맞혔습니다. 틀린 칸은 앞에서 본 그 즉답 회차 하나뿐입니다. H의 none은 숫자보다 나쁩니다. 이 채점은 응답 어딘가에 정답 문자열이 있으면 맞은 것으로 셌습니다. none은 어려운 문제에서 헤매는 본문 안에 정답을 흘리고 끝나기도 합니다. 실제로 쓸 수 있는 답을 준 비율은 이보다 낮습니다. 그런데 none이 위험한 건 정답률보다 예측 불가능성 쪽입니다. 같은 문제를 세 번 부르는데 출력이 7토큰에서 4,711토큰까지 벌어집니다. 비용도 지연도 미리 잡을 수가 없습니다.

그럼 프롬프트로 막으면 어떻게 될까요. 풀이 과정을 쓰지 마라. 검산하지 마라. 최종 답 한 줄만 출력하라.를 붙여 각 한 번씩 불러봤습니다.

문제none 그대로none + 추론 금지
H — 평면기하2,519토큰 · 정답12토큰 · 정답
VH — 조합 계수3,958토큰 · 정답7토큰 · 오답 (72, 정답 70)

폭증은 막힙니다. 다만 H에서 12토큰에 맞힌 건 비비아니 정리의 교과서 값이라 외워서 꺼낸 것입니다. 실제로 세어야 하는 VH에서는 같은 방식이 7토큰에 틀렸습니다. 프롬프트로 비용을 누르면 사고가 필요한 문제에서 정확도를 내주게 됩니다. 같은 두 문제를 low는 570토큰과 697토큰에 3회 모두 맞혔습니다. 맡기실 업무가 어렵지 않다고 판단되면 none에 추론 금지 프롬프트를 얹는 조합이 가장 쌉니다. 다만 그 판단이 틀렸을 때 조용히 오답이 나온다는 점은 감수해야 합니다.

표에는 재미있는 현상이 하나 더 있습니다. xhigh가 high보다 쌉니다 — 네 문제 모두 중앙값이 그랬습니다. 다만 회차 변동이 커서 넷 중 셋은 두 단계의 측정 구간이 겹칩니다.

xhigh를 high보다 비싼 단계로 가정하면 안 된다는 것까지가 확실합니다. 단계 이름의 순서가 비용의 순서와 맞지 않으니, 실제 태스크로 직접 재보는 수밖에 없습니다.

단가와 티어

그럼 단가 자체는 얼마인지 보겠습니다. 모델 카드에만 실려 있습니다.

추론 옵션입력출력캐시 읽기
In-Region$2.20$6.60$0.55
Geo CRIS$2.20$6.60$0.55
Global CRIS (= 서울)$2.00$6.00$0.50

100만 토큰당이고 Standard 티어 기준입니다. 서울에서는 Global만 지원하니 세 번째 줄이 전부입니다. 입력 $2.00, 출력 $6.00입니다. 위 두 줄은 다른 리전 이야기입니다. 캐시 읽기 $0.50은 1장에서 본 대로 적중을 고를 수 없어, 이 단가를 전제로 예산을 낮춰 잡기는 어렵습니다. 다만 캐시 단가가 이미 표에 있다는 건 캐싱 지원이 정리되는 중이라는 신호로 보입니다.

티어는 셋 중에 고를 수 있습니다. 기본은 Standard이고, service_tier를 priority로 주면 우선 처리에 2배flex로 주면 처리 시간을 양보하는 대신 0.5배로 청구됩니다. Reserved는 월 단위 용량 예약이라 계정팀을 거쳐야 하는데 Grok 4.6은 미지원입니다. 서울에서 네 값을 직접 넣어보니 이렇게 나왔습니다.

service_tier 없음   200  service_tier=default
priority            200  service_tier=priority
flex                200  service_tier=flex
reserved            400  unknown variant `reserved`

급하지 않은 배치성 작업이라면 flex 한 줄로 단가가 절반이 됩니다.

2.Guardrails

Bedrock에서 입출력을 걸러내는 표준 수단인 Guardrails는 GPT-5.6에서 Converse API에 붙일 수 있었습니다. Grok 4.6에서는 요청이 거절됩니다.

guardContent로 감싸도, 평문 text로 보내도 같은 에러였습니다.

ValidationException: The model returned the following errors:
Guardrail was enabled but input is in incorrect format.

모델 카드에는 Guardrails 항목 자체가 없어서 지원 여부를 문서로는 알 수 없었는데, 실제로는 이렇게 실패합니다.

방법이 아주 없지는 않습니다. ApplyGuardrail은 모델과 무관한 API라 단독으로 부르면 정상 동작합니다. 그러니 검사와 호출을 나누면 됩니다. 입력을 먼저 ApplyGuardrail로 검사하고, 통과하면 Grok을 호출하고, 응답을 다시 ApplyGuardrail로 검사하는 순서입니다.

코드로는 이렇게 됩니다.

GUARDRAIL_ID, GUARDRAIL_VERSION = "여러분의-guardrail-id", "DRAFT"

def guard(text, source):          # source: "INPUT" 또는 "OUTPUT"
    r = br.apply_guardrail(
        guardrailIdentifier=GUARDRAIL_ID, guardrailVersion=GUARDRAIL_VERSION,
        source=source, content=[{"text": {"text": text}}],
    )
    return r["action"]            # "NONE" 이면 통과, "GUARDRAIL_INTERVENED" 면 차단

def ask(prompt):
    if guard(prompt, "INPUT") != "NONE":
        return "입력이 정책에 걸렸습니다"
    r = br.converse(
        modelId="global.xai.grok-4.6",
        messages=[{"role": "user", "content": [{"text": prompt}]}],
        inferenceConfig={"maxTokens": 512},
    )
    answer = "".join(b["text"] for b in r["output"]["message"]["content"] if "text" in b)
    if guard(answer, "OUTPUT") != "NONE":
        return "응답이 정책에 걸렸습니다"
    return answer

대신 대가가 따릅니다. 우선 호출이 한 번에서 세 번으로 늘어납니다. ApplyGuardrail 왕복을 다섯 번 재보니 중앙값 205밀리초, 최댓값 695밀리초였습니다. 입출력 두 번이면 0.4초쯤 붙는 셈인데 꼬리가 세 배로 벌어집니다. 응답 시간을 계산할 때는 중앙값이 아니라 최댓값을 기준으로 삼아야 합니다.

요금도 검사 횟수만큼 나갑니다. 짧은 요청이면 입출력 두 번 검사에 1,000요청당 $0.30쯤이고, 검사 대상이 길어지면 그만큼 늘어납니다. 어려운 추론 작업이 1,000요청당 $4~$25인 걸 생각하면 큰 값은 아닙니다. 다만 1장의 쉬운 분류 작업은 effort=none으로 $0.17~$0.19였습니다. 그런 워크로드에서는 Guardrails가 모델 호출보다 비쌉니다.

스트리밍은 더 성가십니다. guardrailConfig가 붙는 모델이라면 Bedrock이 토큰이 흐르는 중에도 걸러주는데, Grok에는 그게 안 붙습니다. 결국 응답을 다 받은 뒤에야 검사할 수 있습니다. 흘러가는 도중에 막으려면 청크를 모아가며 ApplyGuardrail을 반복 호출하는 구조를 직접 짜야 하고, 호출 횟수만큼 지연과 요금이 더 붙습니다. 이 방식은 재보지 않았습니다.

3.API Path

tool use는 네 경로 전부에서 동작하고, 구조화 출력도 Converse만 빼면 다 됩니다. 이미지 입력도 받습니다 — 다만 8×8처럼 아주 작은 이미지는 InternalServerException이 났습니다. 서울에서 Grok 4.6을 부르는 길은 하나가 아닙니다. Converse, OpenAI 호환 /openai/v1/chat/completions와 /openai/v1/responses, 그리고 InvokeModel이 다 열려 있습니다.

xAI 모델인데 왜 OpenAI 경로가 있는지 짚고 넘어가겠습니다. OpenAI로 나가는 요청은 없습니다. 호스트는 bedrock-runtime, 인증은 SigV4, model에 넣는 값은 Bedrock 추론 프로파일 ID입니다. 요청과 응답의 모양만 OpenAI 규격을 따릅니다. AWS 문서의 표현대로 OpenAI SDK로 짜둔 코드가 base_url만 바꿔 Bedrock을 부를 수 있게 열어둔 창구입니다.

다만 모델마다 열리는 API가 다릅니다. 같은 문서가 “OpenAI 호환 엔드포인트와 호환되지 않는 모델”이라는 항목을 따로 두고, 그런 모델은 Converse와 Invoke를 쓰라고 안내합니다. 실제로 같은 요청을 모델만 바꿔 보내면 갈립니다.

global.xai.grok-4.6                 200  {"object": "chat.completion", ...}
global.openai.gpt-5.6-terra         400  max_tokens is not supported with this model
global.anthropic.claude-sonnet-4-6  404  The model doesn't exist or doesn't support this API

404는 경로 자체가 그 모델에 안 열려 있다는 뜻입니다. GPT-5.6 Terra의 400은 성격이 다릅니다 — 경로는 열려 있는데 max_tokens를 안 받는 것이라, 파라미터만 맞추면 통합니다. 같은 모델인데 무엇을 고르느냐에 따라 쓸 수 있는 기능과 볼 수 있는 정보가 달라집니다.

ConverseChat CompletionsResponsesInvokeModel
구조화 출력 (response_format)무시됨동작동작동작
추론 토큰 수치안 보임보임보임보임
tool use동작동작동작동작
스트리밍 — 첫 글자까지8.2초9.7초8.3초
Guardrails거절해당 없음해당 없음해당 없음
사전 토큰 계산미지원미지원미지원미지원

스트리밍 줄은 같은 프롬프트로 세 경로를 각각 세 번씩 재서 첫 글자까지 걸린 시간의 중앙값입니다. 경로를 바꿔도 첫 글자가 뜨는 시각은 달라지지 않습니다. 셋 다 추론이 끝나야 텍스트가 나오고, 회차 변동이 경로 차이보다 큽니다.

첫 줄부터 보겠습니다. Converse는 response_format을 무시합니다. additionalModelRequestFields에 넣어도 에러 없이 통과하고, 응답은 마크다운 펜스에 감싸인 JSON으로 옵니다. 스키마에 없는 필드도 섞입니다. 같은 스키마를 /openai/v1/chat/completions로 보내면 {"city":"서울","population":9668465}처럼 옵니다.

그렇다고 Converse에서 구조화 출력을 못 하는 건 아닙니다. 스키마를 toolSpec의 inputSchema에 넣으면 세 번 불러 세 번 다 여분 필드 없이 왔습니다. LangChain의 with_structured_output()도 response_format이 아니라 tool use로 구현돼 있어서 Converse에서 동작합니다.

같은 스키마를 Converse에 넣으면 이렇게 옵니다.

r = br.converse(
    modelId="global.xai.grok-4.6",
    messages=[{"role": "user", "content": [{"text": "서울 인구를 JSON으로"}]}],
    inferenceConfig={"maxTokens": 1024},
    additionalModelRequestFields={
        "response_format": {"type": "json_schema", "json_schema": {
            "name": "pop", "strict": True,
            "schema": {"type": "object",
                       "properties": {"city": {"type": "string"}, "population": {"type": "integer"}},
                       "required": ["city", "population"], "additionalProperties": False}}}
    },
)
print("".join(b["text"] for b in r["output"]["message"]["content"] if "text" in b))
(백틱 세 개)json   ← 모델이 붙인 마크다운 펜스
{
  "city": "서울특별시",
  "country": "대한민국",       ← 스키마에 없는 필드
  "population": 9668465,
  "year": 2023,                ← 스키마에 없는 필드
  "source": "통계청 / 서울시 공식 통계"
}
(백틱 세 개)

마크다운 펜스에 감싸여 오고, additionalProperties: false로 막아둔 country·year·source까지 섞여 있습니다. 에러는 없습니다.

OpenAI 호환 경로는 SigV4로 서명해 부릅니다. 모델 카드는 Bedrock API 키를 발급해 openai SDK에 물리는 방법을 안내하는데, 아래는 키 없이 계정 자격 증명만으로 부르는 형태입니다.

import json, urllib.request, boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest

REGION = "ap-northeast-2"
creds = boto3.Session().get_credentials().get_frozen_credentials()

def call_openai_v1(path, payload):
    """Bedrock의 OpenAI 호환 엔드포인트를 SigV4로 부릅니다. 모델은 Bedrock 모델 ID입니다."""
    url = f"https://bedrock-runtime.{REGION}.amazonaws.com/openai/v1/{path}"
    body = json.dumps(payload)
    req = AWSRequest(method="POST", url=url, data=body,
                     headers={"Content-Type": "application/json"})
    SigV4Auth(creds, "bedrock", REGION).add_auth(req)
    r = urllib.request.Request(url, data=body.encode(), headers=dict(req.headers))
    with urllib.request.urlopen(r, timeout=120) as resp:
        return json.loads(resp.read())

out = call_openai_v1("chat/completions", {
    "model": "global.xai.grok-4.6",
    "messages": [{"role": "user", "content": "서울 인구를 JSON으로"}],
    "max_tokens": 2048,
    "response_format": {"type": "json_schema", "json_schema": {
        "name": "pop", "strict": True,
        "schema": {"type": "object",
                   "properties": {"city": {"type": "string"}, "population": {"type": "integer"}},
                   "required": ["city", "population"], "additionalProperties": False}}},
})

print(out["choices"][0]["message"]["content"])
# {"city":"Seoul","population":9668465}
print(out["usage"]["completion_tokens_details"]["reasoning_tokens"])
# 250, 415, …  호출마다 다릅니다. Converse에서는 아예 안 보이던 값입니다

추론 토큰도 마찬가지입니다. Converse의 usage에는 추론 토큰 항목이 아예 없어서 outputTokens에 뭉쳐 들어온 채 분리되지 않습니다. OpenAI 호환 경로로 부르면 reasoning_tokens로 따로 나옵니다. 1장에서 본 effort 비교를 자기 프롬프트로 다시 하려면 이 수치가 필요합니다.

"usage": {
  "completion_tokens": 200,
  "completion_tokens_details": {"reasoning_tokens": 180},
  "prompt_tokens": 52,
  "prompt_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0}
}

표 마지막 줄만 성격이 다릅니다. CountTokens는 네 경로와 별개인 API라 어느 쪽을 골라도 같고, 부르면 The provided model doesn't support counting tokens.가 옵니다. 요금은 길이를 안 타지만 500K 상한에 걸리는지는 미리 알 수 없습니다. 응답의 input_tokens를 모아 사후 분포로 잡는 수밖에 없습니다.

Responses로 스트리밍하려면 경로만 responses로 바꾸고 stream: true를 주면 됩니다. SSE로 옵니다.

body = {"model": "global.xai.grok-4.6", "input": "한국의 사계절을 각 2문장",
        "max_output_tokens": 600, "stream": True}
url = f"https://bedrock-runtime.{REGION}.amazonaws.com/openai/v1/responses"
req = AWSRequest(method="POST", url=url, data=json.dumps(body),
                 headers={"Content-Type": "application/json"})
SigV4Auth(creds, "bedrock", REGION).add_auth(req)

r = urllib.request.Request(url, data=json.dumps(body).encode(), headers=dict(req.headers))
with urllib.request.urlopen(r, timeout=120) as resp:
    for line in resp:
        if line.startswith(b"data:") and b"[DONE]" not in line:
            print(line.decode().strip()[:80])
# SSE 이벤트 176개

4.문서와 실제

먼저 InvokeModel입니다. API 호환성 표에는 Grok 4.6이 미지원으로 적혀 있는데, 실제로 부르면 200이 옵니다. 여섯 번 불러 여섯 번 다 그랬습니다. 문서에 없는 동작이라 설계를 여기에 얹을 수는 없습니다. 얻을 정보가 Chat Completions와 같으니 그쪽을 쓰면 됩니다.

resp = br.invoke_model(modelId="global.xai.grok-4.6", body=json.dumps({
    "messages": [{"role": "user", "content": "2+2? 숫자만"}], "max_tokens": 512}))
d = json.loads(resp["body"].read())

print(d["object"])                                              # chat.completion
print(d["usage"]["completion_tokens_details"]["reasoning_tokens"])  # 214

다음은 reasoning.effort입니다. 모델 카드에는 none이 없는데 실제로는 동작하고, 쉬운 작업에서는 가장 싼 선택지입니다. 게다가 잘못된 값을 넣으면 모델이 목록을 직접 알려주는데, 그 목록이 문서와도 서로와도 다릅니다.

# 아무 값이나 넣어보면 이 파라미터가 받는 값 전체를 알려줍니다
br.converse(modelId="global.xai.grok-4.6", messages=[...],
            additionalModelRequestFields={"reasoning": {"effort": "banana"}})
# ValidationException: Invalid value: 'banana'. Supported values are:
#   'none', 'low', 'medium', 'high', 'xhigh', and 'max'.     ← 여섯 개

# 그중 'max'를 넣으면, 이 모델에는 없다고 다시 알려줍니다
br.converse(modelId="global.xai.grok-4.6", messages=[...],
            additionalModelRequestFields={"reasoning": {"effort": "max"}})
# ValidationException: Unsupported value: 'max' is not supported with the
#   'global.xai.grok-4.6' model. Supported values are:
#   'none', 'low', 'medium', 'high', and 'xhigh'.            ← 다섯 개

max는 다른 모델에는 있지만 4.6에서는 못 씁니다. 결국 이 모델에서 쓸 수 있는 값은 문서의 넷에 none을 더한 다섯입니다. 문서에 없는 값이 동작하는 셈이라, 목록은 직접 넣어보고 확인하는 편이 확실합니다.

5.Framework

에이전트 프레임워크에 붙이는 건 쉽습니다. Strands, langchain-aws, LangGraph 모두 model_id만 global.xai.grok-4.6으로 갈아끼우면 그대로 동작합니다. 최소 예제로 각각 한 번씩 확인한 결과입니다. 앞 글에서 같은 클래스에 GPT-5.6을 꽂았을 때는 “저를 만든 회사는 OpenAI입니다”가 나왔는데, 이번에는 xAI라고 답합니다. 문제는 스트리밍입니다. langchain-aws는 xai 프로바이더에 대해 스트리밍을 자동으로 끕니다.

Streaming disabled for model 'global.xai.grok-4.6': provider 'xai' is not
verified as streaming-capable with ChatBedrockConverse, so calls will fall
back to the non-streaming Converse API.

문서대로만 쓰면 예외는 안 납니다. 대신 스트리밍이 조용히 사라집니다. ChatBedrockConverse(...).stream()을 그대로 부르면 청크가 하나만 옵니다 — 생성이 다 끝난 뒤에 전량이 한 번에 떨어집니다. 세 번 재서 6.55초·9.13초·15.19초였고, 그 시간 동안 화면에 아무것도 못 흘립니다. 같은 조건에서 boto3의 converse_stream을 직접 부르면 delta가 160~218개로 나뉘어 옵니다. 예외 대신 기능이 사라지는 쪽이라 눈치채기 더 어렵습니다.

GPT-5.6 Terra에서는 disable_streaming=False로 켜주면 정상 동작했습니다. 추론 delta를 흘리지 않는 모델이었기 때문입니다. Grok에서는 켜는 순간 터집니다.

IndexError: list index out of range
  langchain_aws/chat_models/bedrock_converse.py:2506 in _parse_stream_event
  **_bedrock_to_lc([event["contentBlockDelta"]["delta"]])[0]

원인은 delta 종류에 있습니다. Grok은 추론을 암호화한 채로 흘리는데, langchain-aws가 그 형태를 처리하지 못합니다. 추론이 켜져 있는 한 매번 이 경로를 탑니다.

설정delta 종류stream() 결과
기본 (추론 켜짐)reasoningContentIndexError
effort=nonetext청크 13개, 정상

Grok만의 문제는 아닙니다. 추론을 가려서 보내는 모델이면 같은 지점에서 걸립니다 — GPT-5.6 계열의 Sol과 Luna도 그렇습니다. 이 문제는 제가 langchain-aws에 이슈로 올려뒀습니다 — #1223입니다. 어느 함수가 무엇을 놓치는지, 자격 증명 없이 재현하는 방법까지 정리해뒀습니다. 진행 상황도 그쪽에 남습니다.

쓰는 쪽에서는 이 정도만 알면 됩니다.

from langchain_aws import ChatBedrockConverse

# 추론을 켠 채 스트리밍을 강제하면 IndexError
ChatBedrockConverse(model="global.xai.grok-4.6", region_name="ap-northeast-2",
                    disable_streaming=False).stream("두 문장으로 인사")

# 추론을 끄면 정상 — 청크 25개
ChatBedrockConverse(
    model="global.xai.grok-4.6", region_name="ap-northeast-2",
    disable_streaming=False,
    additional_model_request_fields={"reasoning": {"effort": "none"}},
).stream("두 문장으로 인사")

Strands는 같은 상황에서 멀쩡합니다. 네 모델을 stream_async로 돌려봤더니 전부 정상이었고, Grok과 Sol에서는 가려진 추론 이벤트까지 받아냈습니다.

모델Strandslangchain-aws
Grok 4.6이벤트 434개 (가려진 추론 1)IndexError
GPT-5.6 Sol이벤트 294개 (가려진 추론 1)IndexError
GPT-5.6 Terra이벤트 439개정상
Claude Sonnet 5이벤트 251개정상

부르는 쪽 코드는 이렇습니다. langchain-aws처럼 스트리밍을 따로 켜줄 필요가 없습니다.

from strands import Agent
from strands.models import BedrockModel

agent = Agent(model=BedrockModel(model_id="global.xai.grok-4.6",
                                 region_name="ap-northeast-2"),
              callback_handler=None)

async for ev in agent.stream_async("한국의 사계절을 각 2문장으로"):
    if isinstance(ev, dict) and "data" in ev:
        print(ev["data"], end="")
# 이벤트 476개 (텍스트 232, 가려진 추론 1)

Strands의 event_loop/streaming.py에는 redactedContent 전용 분기가 있고 그걸 실어 나르는 이벤트 타입도 따로 있습니다.

# strands-py/src/strands/event_loop/streaming.py (266행)
elif redacted_content := delta_content["reasoningContent"].get("redactedContent"):
    state["redactedContent"] = state.get("redactedContent", b"") + redacted_content

같은 Bedrock API, 같은 모델인데 되고 안 되고는 라이브러리에서 갈립니다. Strands는 AWS가 공개한 에이전트 SDK이고, Amazon Q Developer·AWS Glue 같은 AWS 팀 서비스가 프로덕션에서 쓰고 있습니다. Bedrock이 새 응답 형식을 내보내면 대응이 가장 빠른 프레임워크 중 하나라는 뜻입니다.

캐싱은 사정이 다릅니다. 캐시 포인트를 넣는 방식은 양쪽 다 거절당합니다 — 1장에서 본 그 AccessDeniedException입니다. 반면 Strands의 CacheConfig(strategy="auto")는 모델을 보고 캐시 포인트를 아예 안 넣어서 요청이 통과합니다. 대신 경고를 남깁니다.

model_id=<global.xai.grok-4.6> | cache_config is enabled but this model
does not support automatic caching
from strands.models.bedrock import CacheConfig

model = BedrockModel(model_id="global.xai.grok-4.6", region_name="ap-northeast-2",
                     cache_config=CacheConfig(strategy="auto"))
방식Grok 4.6
Strands cache_prompt="default"AccessDeniedException
langchain-aws create_cache_point()AccessDeniedException
Strands CacheConfig(strategy="auto")통과. 단 미지원 경고

캐싱을 쓰던 코드를 Grok으로 갈아끼우면 그 자리에서 죽습니다. auto로 잡아둔 경우만 살아납니다. 다만 auto가 캐싱을 켜주는 것은 아닙니다. 1장에서 본 그 암묵 캐싱이 그대로 걸릴 뿐이고, 프레임워크 설정으로 달라지지 않습니다.

langchain-aws에서는 추론과 스트리밍을 동시에 쓸 수 없습니다. 둘 다 필요하면 라이브러리를 거치지 않고 converse_stream을 직접 불러야 합니다. 막힌 쪽은 Bedrock이 아니라 라이브러리이기 때문입니다 — 추론을 켠 채 같은 프롬프트를 세 번 불러보니 delta가 160·218·213개로 정상 분할돼 왔습니다. 앞서 올린 이슈는 2026년 8월 24일 기준 아직 열려 있습니다.

6.도입 판단

global.*로 부른 요청이 실제로 어디로 가는지는 CloudTrail이 알려줍니다. 이날 서울에서 부른 180건을 전부 조회했습니다. 성공한 167건에는 additionalEventData.inferenceRegion이 빠짐없이 있었고, 값은 모두 us-west-2였습니다. 나머지 13건은 실패한 호출이라 필드 자체가 없습니다. 경로는 가리지 않습니다 — 다섯 경로 모두 같은 값으로 기록됩니다.

Converse         source=ap-northeast-2  inferenceRegion=us-west-2  global.xai.grok-4.6
ConverseStream   source=ap-northeast-2  inferenceRegion=us-west-2  global.xai.grok-4.6
InvokeModel      source=ap-northeast-2  inferenceRegion=us-west-2  global.xai.grok-4.6

한 계정에서 한 시점에 본 결과이므로 “항상 오리건”으로 읽으면 안 됩니다. Global CRIS의 목적지는 지원되는 상용 리전 전체이고, 리전이 추가되면 그 집합도 바뀝니다. 다만 서울에서 부른 요청이 서울에서 처리되지 않는다는 사실은 성공한 167건이 예외 없이 같은 방향을 가리킵니다. 국내 처리가 요건이라면 여기서 판단이 끝납니다. 서울 In-Region 경로가 없으니 우회할 방법도 없습니다.

앞에서 확인한 것들을 요건 쪽에서 다시 세우면 이렇게 됩니다. 여덟 줄 중 다섯은 방법이 있습니다.

요건판단
데이터가 국내에서 처리돼야 한다아직은 쓸 수 없습니다. 서울 In-Region 경로가 없어 우회할 방법이 없습니다
Guardrails로 입출력을 걸러야 한다호출을 셋으로 나누면 됩니다. 다만 스트리밍 중 차단은 힘듭니다
긴 프롬프트를 캐싱해 단가를 낮춰야 한다예산에 반영하기는 어렵습니다. 캐시 경계를 지정할 수 없고 적중도 고를 수 없습니다
스키마가 고정된 JSON을 받아야 한다됩니다. Converse는 response_format을 무시하므로 tool use를 쓰거나 다른 경로로 부르면 됩니다
사전에 토큰 수를 세야 한다아직은 셀 수 없습니다. CountTokens를 이 모델에 쓸 수 없습니다
답이 정해진 대량 분류·추출을 돌린다됩니다. 단 모델에게도 쉬운 입력일 때만 effort=none이 쌉니다
난도 높은 추론을 시킨다됩니다. 단 none을 피하고 low부터 재보는 편이 낫습니다
LangChain으로 스트리밍 UI를 만든다추론을 끄면 됩니다. 둘 다 필요하면 converse_stream을 직접 불러야 합니다

서울에서 Grok 4.6을 부를 수 있게 된 건 분명한 진전입니다. 다만 GPT-5.6 때 정리해둔 설정을 그대로 옮기면 어긋납니다. 위치 제약이 같아서 같은 자리에 있는 것처럼 보이지만, 캐싱과 effort가 움직이는 방식도, Guardrails를 거는 방법도 다릅니다. 막힌다고 적은 항목은 대부분 시간이 풀어줄 것들입니다. 공개 가격 페이지에 4.6이 아직 없고, langchain-aws의 스트리밍 문제도 이슈로 열려 있습니다. 다만 지금 도입을 판단해야 한다면, 그 기준은 GPT-5.6 때와 달라야 합니다.

부록: 재현 환경

숫자는 계정과 시점을 탑니다. 아래가 이 글의 측정 조건입니다.

항목
측정일2026년 8월 20~21일
리전ap-northeast-2
모델global.xai.grok-4.6 (startOfLifeTime 2026-08-19T17:00:00Z)
단가입력 $2.00 / 출력 $6.00 (100만 토큰당, Global CRIS)
단가 출처모델 카드. 공개 가격 페이지와 Price List API에는 4.6이 아직 없습니다
실행 환경Python 3.14.6 / boto3 1.43.75 / aws-cli 2.34.55
프레임워크strands-agents 1.52.0 / langchain-aws 1.7.2·1.7.3 / langchain-core 1.6.0 / langgraph 1.2.11

5장의 스트리밍 버그는 langchain-aws 1.7.3과 저장소 main에서도 재현됩니다.

측정 횟수와 한계는 이렇습니다.

  • effort별 비용(1장) — 문제 4종 × 단계 5종 × 각 3회입니다. 문제는 감정 분류, 연립방정식, 평면기하, 조합 계수 네 가지이고 전부 입력이 짧고 출력이 지배적인 형태입니다. 입력이 긴 워크로드에는 그대로 대입되지 않습니다.
  • 정답 판정(1장) — 응답 안에 정답 문자열이 있으면 맞은 것으로 셌습니다. none처럼 헤매다 정답을 흘리고 끝나는 경우가 정답으로 잡혔으므로, 쓸 수 있는 답을 준 비율은 표보다 낮습니다.
  • 지연(1장·2장) — effort 측정은 네 요청을 동시에 돌리며 잰 값이라 단일 요청 응답 시간이 아닙니다. ApplyGuardrail 왕복은 5회 측정입니다.
  • 캐싱(1장) — 명시적 캐시 지정은 Converse가 AccessDenied, Responses가 400입니다. 암묵 캐싱만 걸리는데 여러 조건에서 58회를 불러 적중 26회였고 시행별로 4회 중 0회부터 4회 중 4회까지 흔들렸습니다. 이 수치를 기대 적중률로 쓸 수는 없습니다.
  • 데이터 처리 위치(6장) — 한 계정, 한 시점, 180건입니다(성공 167건). Global CRIS의 목적지는 바뀔 수 있습니다.
  • 프레임워크(5장) — 각 1회 확인입니다. 라이브러리 소스도 함께 읽어 원인을 확인했습니다. CacheConfig(strategy="auto")는 별도로 auto·미설정 각 14회씩 비교했습니다(2026-08-21, strands-agents 1.52.0). 초기에 auto가 캐싱을 막는 것으로 적었다가, 시행을 늘려 설정과 무관하다는 것을 확인하고 고쳤습니다.

effort 표의 값은 3회 중앙값입니다. 어려운 문제일수록 회차 간 변동이 큽니다. H 문제의 none은 2,737토큰에서 16,384토큰까지 여섯 배 벌어졌고, VH의 none은 7토큰과 4,711토큰이 같이 나왔습니다. 비용 상한은 중앙값이 아니라 최댓값으로 잡아야 합니다.

참고 링크

이 글이 근거로 삼은 문서와 저장소입니다. 문서는 갱신되므로 확인 시점(2026년 8월 20~21일)과 다를 수 있습니다.

AWS 문서

라이브러리

앞 글


AWS의 파트너사인 MegazoneCloud 소속으로 AWS AI Ambassador 후보로 활동하며 작성한 내용입니다. AWS의 서비스를 소개하고, 실제 업무에서 사용한 사례들에 대한 내용들을 담고 있습니다.
Written By. Bogeun Kim

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다