Gemini API 토큰 이해하기

Gemini API 토큰 이해하기 (Tokens)

Gemini와 같은 생성형 AI 모델은 입력과 출력을 토큰(token) 이라는 단위로 처리해요.

Gemini 모델의 경우 토큰 하나는 약 4자의 문자에 해당해요. 100토큰은 영어 단어 약 60~80개와 같습니다.

출처: 문서

본문

토큰이란?

토큰은 z 같은 단일 문자일 수도 있고 cat 같은 완전한 단어일 수도 있어요. 긴 단어는 여러 토큰으로 쪼개집니다. 모델이 사용하는 모든 토큰의 집합을 어휘(vocabulary)라고 하고, 텍스트를 토큰으로 나누는 과정을 토큰화(tokenization) 라고 해요.

과금(billing)이 활성화되면 Gemini API 호출 비용이 입력·출력 토큰 수에 따라 결정되므로, 토큰을 세는 방법을 아는 것이 도움이 돼요. Colab 노트북에서 토큰 계산을 직접 시험해 볼 수도 있습니다.

ai.google.dev에서 보기 | Colab 노트북 시도 | GitHub에서 노트북 보기

토큰 세기

Gemini API로 들어가고 나오는 모든 입력은 토큰화됩니다. 텍스트, 이미지 파일, 기타 비텍스트 모달리티를 포함해서요.

토큰을 세는 방법은 다음과 같아요.

  • 요청 입력과 함께 count_tokens 호출하기. 입력만의 총 토큰 수를 반환해요. 입력을 모델에 보내기 전에 이 호출로 요청 크기를 확인할 수 있어요.

  • generate_content 호출 후 response 객체의 usage_metadata 속성 사용하기. 입력과 출력 모두의 총 토큰 수인 total_token_count를 반환해요. 입력과 출력의 토큰 수를 따로도 반환해요. prompt_token_count(입력 토큰)와 candidates_token_count(출력 토큰)이에요.

    thinking 모델을 사용한다면 thinking 과정에서 사용된 토큰이 thoughts_token_count로 반환돼요. 또 Context caching을 사용하면 캐시된 토큰 수가 cached_content_token_count에 들어와요.

텍스트 토큰 세기

count_tokens를 텍스트 전용 입력으로 호출하면 입력만의 텍스트 토큰 수(total_tokens)를 반환해요. generate_content를 호출하기 전에 이 호출을 해서 요청 크기를 확인할 수 있어요.

또 다른 방법은 generate_content를 호출한 뒤 response 객체의 usage_metadata 속성에서 다음 값을 얻는 거예요.

  • 입력(prompt_token_count), 캐시된 콘텐츠(cached_content_token_count), 출력(candidates_token_count)의 각각 토큰 수
  • thinking 과정의 토큰 수(thoughts_token_count)
  • 입력과 출력 모두의 총 토큰 수(total_token_count)
from google import genai

client = genai.Client()
prompt = "The quick brown fox jumps over the lazy dog."

total_tokens = client.models.count_tokens(
    model="gemini-3.8-flash", contents=prompt
)
print("total_tokens: ", total_tokens)

response = client.models.generate_content(
    model="gemini-3.8-flash", contents=prompt
)

print(response.usage_metadata)
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({});
const prompt = "The quick brown fox jumps over the lazy dog.";

async function main() {
  const countTokensResponse = await ai.models.countTokens({
    model: "gemini-3.8-flash",
    contents: prompt,
  });
  console.log(countTokensResponse.totalTokens);

  const generateResponse = await ai.models.generateContent({
    model: "gemini-3.8-flash",
    contents: prompt,
  });
  console.log(generateResponse.usageMetadata);
}

await main();
ctx := context.Background()
client, err := genai.NewClient(ctx, nil)

// Convert prompt to a slice of *genai.Content using the helper.
contents := []*genai.Content{
  genai.NewContentFromText(prompt, genai.RoleUser),
}
countResp, err := client.Models.CountTokens(ctx, "gemini-3.8-flash", contents, nil)
if err != nil {
  return err
}
fmt.Println("total_tokens:", countResp.TotalTokens)

response, err := client.Models.GenerateContent(ctx, "gemini-3.8-flash", contents, nil)
if err != nil {
  log.Fatal(err)
}
usageMetadata, err := json.MarshalIndent(response.UsageMetadata, "", "  ")
if err != nil {
  log.Fatal(err)
}
fmt.Println(string(usageMetadata))

다중 턴(채팅) 토큰 세기

count_tokens를 채팅 히스토리와 함께 호출하면 채팅의 각 역할에서 온 텍스트의 총 토큰 수(total_tokens)를 반환해요.

또 다른 방법은 send_message를 호출한 뒤 response 객체의 usage_metadata 속성에서 다음 값을 얻는 거예요.

  • 입력(prompt_token_count), 캐시된 콘텐츠(cached_content_token_count), 출력(candidates_token_count)의 각각 토큰 수
  • thinking 과정의 토큰 수(thoughts_token_count)
  • 입력과 출력 모두의 총 토큰 수(total_token_count)

다음 대화 턴이 얼마나 큰지 이해하려면 count_tokens를 호출할 때 히스토리에 그 턴을 추가해야 해요.

from google import genai
from google.genai import types

client = genai.Client()

chat = client.chats.create(
    model="gemini-3.8-flash",
    history=[
        types.Content(
            role="user", parts=[types.Part(text="Hi my name is Bob")]
        ),
        types.Content(role="model", parts=[types.Part(text="Hi Bob!")]),
    ],
)

print(
    client.models.count_tokens(
        model="gemini-3.8-flash", contents=chat.get_history()
    )
)

response = chat.send_message(
    message="In one sentence, explain how a computer works to a young child."
)
print(response.usage_metadata)

extra = types.UserContent(
    parts=[
        types.Part(
            text="What is the meaning of life?",
        )
    ]
)
history = [*chat.get_history(), extra]
print(client.models.count_tokens(model="gemini-3.8-flash", contents=history))
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({});

async function main() {
  const history = [
    { role: "user", parts: [{ text: "Hi my name is Bob" }] },
    { role: "model", parts: [{ text: "Hi Bob!" }] },
  ];
  const chat = ai.chats.create({
    model: "gemini-3.8-flash",
    history: history,
  });

  const countTokensResponse = await ai.models.countTokens({
    model: "gemini-3.8-flash",
    contents: chat.getHistory(),
  });
  console.log(countTokensResponse.totalTokens);

  const chatResponse = await chat.sendMessage({
    message: "In one sentence, explain how a computer works to a young child.",
  });
  console.log(chatResponse.usageMetadata);

  const extraMessage = {
    role: "user",
    parts: [{ text: "What is the meaning of life?" }],
  };
  const combinedHistory = [...chat.getHistory(), extraMessage];
  const combinedCountTokensResponse = await ai.models.countTokens({
    model: "gemini-3.8-flash",
    contents: combinedHistory,
  });
  console.log(
    "Combined history token count:",
    combinedCountTokensResponse.totalTokens,
  );
}

await main();
ctx := context.Background()
client, err := genai.NewClient(ctx, nil)

history := []*genai.Content{
  {Role: genai.RoleUser, Parts: []*genai.Part({Text: "Hi my name is Bob"})},
  {Role: genai.RoleModel, Parts: []*genai.Part({Text: "Hi Bob!"})},
}
chat, err := client.Chats.Create(ctx, "gemini-3.8-flash", nil, history)
if err != nil {
  log.Fatal(err)
}

firstTokenResp, err := client.Models.CountTokens(ctx, "gemini-3.8-flash", chat.History(false), nil)
if err != nil {
  log.Fatal(err)
}
fmt.Println(firstTokenResp.TotalTokens)

resp, err := chat.SendMessage(ctx, genai.NewPartFromText("In one sentence, explain how a computer works to a young child."))
if err != nil {
  log.Fatal(err)
}
fmt.Printf("%#v\n", resp.UsageMetadata)

extra := genai.NewContentFromText("What is the meaning of life?", genai.RoleUser)
hist := chat.History(false)
hist = append(hist, extra)

secondTokenResp, err := client.Models.CountTokens(ctx, "gemini-3.8-flash", hist, nil)
if err != nil {
  log.Fatal(err)
}
fmt.Println(secondTokenResp.TotalTokens)

멀티모달 토큰 세기

Gemini API로 들어가는 모든 입력은 토큰화됩니다. 텍스트, 이미지 파일, 기타 비텍스트 모달리티를 포함해서요. Gemini API가 멀티모달 입력을 처리할 때 알아둘 토큰화 핵심 사항은 다음과 같아요.

  • 두 차원이 모두 384픽셀 이하인 이미지 입력은 258 토큰으로 계산돼요. 한쪽 또는 양쪽 차원이 더 큰 이미지는 필요에 따라 768x768 픽셀 타일로 잘리고 확장되며, 각 타일이 258 토큰으로 계산돼요.
  • 동영상과 오디오 파일은 다음과 같은 고정 비율로 토큰으로 변환돼요. 동영상은 초당 263토큰, 오디오는 초당 32토큰이에요. 이 비율은 정적(static) 처리(기본값)에 적용돼요. 에이전틱(agentic) 처리에서는 토큰 사용량이 달라져요. 처리 모드별 동영상 토큰 사용량을 참고하세요.
미디어 해상도

Gemini 3 모델은 media_resolution 파라미터로 멀티모달 비전 처리에 대한 세밀한 제어를 도입했어요. media_resolution 파라미터는 입력 이미지나 동영상 프레임당 할당되는 최대 토큰 수를 결정해요. 해상도가 높을수록 미세한 텍스트를 읽거나 작은 세부 사항을 식별하는 모델의 능력이 좋아지지만, 토큰 사용량과 지연 시간은 늘어나요.

이 파라미터와 토큰 계산에 미치는 영향에 대한 자세한 내용은 media resolution 가이드를 참고하세요.

이미지 파일

count_tokens를 텍스트+이미지 입력으로 호출하면 입력만의 텍스트와 이미지 합산 토큰 수(total_tokens)를 반환해요. 이 호출을 generate_content 전에 해서 요청 크기를 확인할 수 있어요. 텍스트와 파일을 각각 따로 count_tokens로 호출할 수도 있어요.

또 다른 방법은 generate_content를 호출한 뒤 response 객체의 usage_metadata 속성에서 다음 값을 얻는 거예요.

  • 입력(prompt_token_count), 캐시된 콘텐츠(cached_content_token_count), 출력(candidates_token_count)의 각각 토큰 수
  • thinking 과정의 토큰 수(thoughts_token_count)
  • 입력과 출력 모두의 총 토큰 수(total_token_count)

File API로 업로드한 이미지를 사용하는 예시:

from google import genai

client = genai.Client()
prompt = "Tell me about this image"
your_image_file = client.files.upload(file=media / "organ.jpg")

print(
    client.models.count_tokens(
        model="gemini-3.8-flash", contents=[prompt, your_image_file]
    )
)

response = client.models.generate_content(
    model="gemini-3.8-flash", contents=[prompt, your_image_file]
)
print(response.usage_metadata)
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({});
const prompt = "Tell me about this image";

async function main() {
  const organ = await ai.files.upload({
    file: path.join(media, "organ.jpg"),
    config: { mimeType: "image/jpeg" },
  });

  const countTokensResponse = await ai.models.countTokens({
    model: "gemini-3.8-flash",
    contents: createUserContent([
      prompt,
      createPartFromUri(organ.uri, organ.mimeType),
    ]),
  });
  console.log(countTokensResponse.totalTokens);

  const generateResponse = await ai.models.generateContent({
    model: "gemini-3.8-flash",
    contents: createUserContent([
      prompt,
      createPartFromUri(organ.uri, organ.mimeType),
    ]),
  });
  console.log(generateResponse.usageMetadata);
}

await main();
ctx := context.Background()
client, err := genai.NewClient(ctx, nil)

file, err := client.Files.UploadFromPath(
  ctx, 
  filepath.Join(getMedia(), "organ.jpg"), 
  &genai.UploadFileConfig{
    MIMEType : "image/jpeg",
  },
)
if err != nil {
  log.Fatal(err)
}
parts := []*genai.Part{
  genai.NewPartFromText("Tell me about this image"),
  genai.NewPartFromURI(file.URI, file.MIMEType),
}
contents := []*genai.Content{
  genai.NewContentFromParts(parts, genai.RoleUser),
}

tokenResp, err := client.Models.CountTokens(ctx, "gemini-3.8-flash", contents, nil)
if err != nil {
  log.Fatal(err)
}
fmt.Println("Multimodal image token count:", tokenResp.TotalTokens)

response, err := client.Models.GenerateContent(ctx, "gemini-3.8-flash", contents, nil)
if err != nil {
  log.Fatal(err)
}
usageMetadata, err := json.MarshalIndent(response.UsageMetadata, "", "  ")
if err != nil {
  log.Fatal(err)
}
fmt.Println(string(usageMetadata))

이미지를 인라인 데이터로 제공하는 예시:

from google import genai
import PIL.Image

client = genai.Client()
prompt = "Tell me about this image"
your_image_file = PIL.Image.open(media / "organ.jpg")

print(
    client.models.count_tokens(
        model="gemini-3.8-flash", contents=[prompt, your_image_file]
    )
)

response = client.models.generate_content(
    model="gemini-3.8-flash", contents=[prompt, your_image_file]
)
print(response.usage_metadata)
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({});
const prompt = "Tell me about this image";
const imageBuffer = fs.readFileSync(path.join(media, "organ.jpg"));

const imageBase64 = imageBuffer.toString("base64");

const contents = createUserContent([
  prompt,
  createPartFromBase64(imageBase64, "image/jpeg"),
]);

async function main() {
  const countTokensResponse = await ai.models.countTokens({
    model: "gemini-3.8-flash",
    contents: contents,
  });
  console.log(countTokensResponse.totalTokens);

  const generateResponse = await ai.models.generateContent({
    model: "gemini-3.8-flash",
    contents: contents,
  });
  console.log(generateResponse.usageMetadata);
}

await main();
ctx := context.Background()
client, err := genai.NewClient(ctx, nil)

imageBytes, err := os.ReadFile("organ.jpg")
if err != nil {
    log.Fatalf("Failed to read image file: %v", err)
}
parts := []*genai.Part{
  genai.NewPartFromText("Tell me about this image"),
  {
        InlineData: &genai.Blob{
              MIMEType: "image/jpeg",
              Data:     imageBytes,
        },
  },
}
contents := []*genai.Content{
  genai.NewContentFromParts(parts, genai.RoleUser),
}

tokenResp, err := client.Models.CountTokens(ctx, "gemini-3.8-flash", contents, nil)
if err != nil {
  log.Fatal(err)
}
fmt.Println("Multimodal image token count:", tokenResp.TotalTokens)

response, err := client.Models.GenerateContent(ctx, "gemini-3.8-flash", contents, nil)
if err != nil {
  log.Fatal(err)
}
usageMetadata, err := json.MarshalIndent(response.UsageMetadata, "", "  ")
if err != nil {
  log.Fatal(err)
}
fmt.Println(string(usageMetadata))
동영상 또는 오디오 파일

오디오와 동영상은 각각 다음과 같은 고정 비율로 토큰으로 변환돼요.

  • 동영상: 초당 263토큰
  • 오디오: 초당 32토큰

count_tokens를 텍스트+동영상/오디오 입력으로 호출하면 입력만의 텍스트와 동영상/오디오 파일 합산 토큰 수(total_tokens)를 반환해요. 이 호출을 generate_content 전에 해서 요청 크기를 확인할 수 있어요. 텍스트와 파일을 각각 따로 count_tokens로 호출할 수도 있어요.

또 다른 방법은 generate_content를 호출한 뒤 response 객체의 usage_metadata 속성에서 다음 값을 얻는 거예요.

  • 입력(prompt_token_count), 캐시된 콘텐츠(cached_content_token_count), 출력(candidates_token_count)의 각각 토큰 수
  • thinking 과정의 토큰 수(thoughts_token_count)
  • 입력과 출력 모두의 총 토큰 수(total_token_count)
from google import genai
import time

client = genai.Client()
prompt = "Tell me about this video"
your_file = client.files.upload(file=media / "Big_Buck_Bunny.mp4")

while not your_file.state or your_file.state.name != "ACTIVE":
    print("Processing video...")
    print("File state:", your_file.state)
    time.sleep(5)
    your_file = client.files.get(name=your_file.name)

print(
    client.models.count_tokens(
        model="gemini-3.8-flash", contents=[prompt, your_file]
    )
)

response = client.models.generate_content(
    model="gemini-3.8-flash", contents=[prompt, your_file]
)
print(response.usage_metadata)
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({});
const prompt = "Tell me about this video";

async function main() {
  let videoFile = await ai.files.upload({
    file: path.join(media, "Big_Buck_Bunny.mp4"),
    config: { mimeType: "video/mp4" },
  });

  while (!videoFile.state || videoFile.state.toString() !== "ACTIVE") {
    console.log("Processing video...");
    console.log("File state: ", videoFile.state);
    await sleep(5000);
    videoFile = await ai.files.get({ name: videoFile.name });
  }

  const countTokensResponse = await ai.models.countTokens({
    model: "gemini-3.8-flash",
    contents: createUserContent([
      prompt,
      createPartFromUri(videoFile.uri, videoFile.mimeType),
    ]),
  });
  console.log(countTokensResponse.totalTokens);

  const generateResponse = await ai.models.generateContent({
    model: "gemini-3.8-flash",
    contents: createUserContent([
      prompt,
      createPartFromUri(videoFile.uri, videoFile.mimeType),
    ]),
  });
  console.log(generateResponse.usageMetadata);
}

await main();
ctx := context.Background()
client, err := genai.NewClient(ctx, nil)

file, err := client.Files.UploadFromPath(
  ctx,
  filepath.Join(getMedia(), "Big_Buck_Bunny.mp4"),
  &genai.UploadFileConfig{
    MIMEType : "video/mp4",
  },
)
if err != nil {
  log.Fatal(err)
}

for file.State == genai.FileStateUnspecified || file.State != genai.FileStateActive {
  fmt.Println("Processing video...")
  fmt.Println("File state:", file.State)
  time.Sleep(5 * time.Second)

  file, err = client.Files.Get(ctx, file.Name, nil)
  if err != nil {
    log.Fatal(err)
  }
}

parts := []*genai.Part{
  genai.NewPartFromText("Tell me about this video"),
  genai.NewPartFromURI(file.URI, file.MIMEType),
}
contents := []*genai.Content{
  genai.NewContentFromParts(parts, genai.RoleUser),
}

tokenResp, err := client.Models.CountTokens(ctx, "gemini-3.8-flash", contents, nil)
if err != nil {
  log.Fatal(err)
}
fmt.Println("Multimodal video/audio token count:", tokenResp.TotalTokens)
response, err := client.Models.GenerateContent(ctx, "gemini-3.8-flash", contents, nil)
if err != nil {
  log.Fatal(err)
}
usageMetadata, err := json.MarshalIndent(response.UsageMetadata, "", "  ")
if err != nil {
  log.Fatal(err)
}
fmt.Println(string(usageMetadata))
처리 모드별 동영상 토큰 사용량

동영상의 토큰 사용량은 처리 모드에 따라 달라져요.

처리 모드 토큰 계산 일반적인 사용
정적 (Static) (기본값) 기본적으로 초당 약 100토큰(저해상도) 또는 초당 약 300토큰(고해상도). 모든 프레임이 1 FPS로 샘플링. 예측 가능하며 동영상 길이에 비례.
에이전틱 (Agentic) 콘텐츠 복잡도에 따라 달라짐. 모델은 프롬프트에 답하는 데 필요한 전사본과/또는 프레임과/또는 오디오만 로드. 긴 형식 콘텐츠의 경우 최대 88% 토큰 절감.

에이전틱 처리에서는 정적 모드에서 약 1.08M 토큰을 쓸 1시간짜리 강의가 프롬프트와 콘텐츠에 따라 약 108K 토큰만 사용할 수도 있어요.

요청의 실제 토큰 사용량을 확인하려면 응답의 usage_metadata 필드를 사용하세요. UsageMetadata에서 에이전틱 동영상 토큰은 다음 필드에 걸쳐 보고돼요.

  • 초기 프롬프트 (동영상 참조 + 사용자 프롬프트): prompt_token_count
  • 탐색 thinking (Navigation thinking): thoughts_token_count
  • 요청 시 로드되는 전사본·프레임·오디오: tool_use_prompt_token_count
  • 최종 답변: candidates_token_count

thinking 토큰 세기

참고: API는 무료·유료 계층 모두에서 요약(summaries) 을 제공해요. thought signature는 이후 대화 턴에서 다시 보낼 때 입력 토큰 수(와 비용)를 늘려요.

thinking을 켜면 응답 가격은 출력 토큰과 thinking 토큰의 합이에요. 생성된 전체 thinking 토큰 수는 thoughtsTokenCount 필드(또는 SDK 상당 필드)에서 가져올 수 있어요.

# ...
print("Thoughts tokens:", response.usage_metadata.thoughts_token_count)
print("Output tokens:", response.usage_metadata.candidates_token_count)
// ...
console.log(`Thoughts tokens: ${response.usageMetadata.thoughtsTokenCount}`);
console.log(`Output tokens: ${response.usageMetadata.candidatesTokenCount}`);
// ...
fmt.Println("Thoughts tokens:", response.UsageMetadata.ThoughtsTokenCount)
fmt.Println("Output tokens:", response.UsageMetadata.CandidatesTokenCount)

thinking 모델은 최종 응답의 품질을 높이기 위해 완전한 생각(full thoughts)을 생성한 뒤, thought 과정에 대한 통찰을 제공하는 요약을 출력해요. 그래서 API는 요약만 출력하지만, 요약을 만들기 위해 모델이 생성한 전체 thinking 토큰을 기준으로 가격을 책정해요.

thinking을 구성하는 방법은 Gemini thinking 가이드에서 더 배울 수 있어요.

컨텍스트 윈도우

Gemini API를 통해 제공되는 모델은 토큰 단위로 측정되는 컨텍스트 윈도우를 가져요. 컨텍스트 윈도우는 얼마나 많은 입력을 제공할 수 있고 모델이 얼마나 많은 출력을 생성할 수 있는지 정의해요. 컨텍스트 윈도우의 크기는 models.get 엔드포인트를 호출하거나 models 문서에서 확인할 수 있어요.

from google import genai

client = genai.Client()
model_info = client.models.get(model="gemini-3.8-flash")
print(f"{model_info.input_token_limit=}")
print(f"{model_info.output_token_limit=}")
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({});

async function main() {
  const modelInfo = await ai.models.get({model: 'gemini-3.8-flash'});
  console.log(modelInfo.inputTokenLimit);
  console.log(modelInfo.outputTokenLimit);
}

await main();
ctx := context.Background()
client, err := genai.NewClient(ctx, nil)
if err != nil {
  log.Fatal(err)
}
modelInfo, err := client.ModelInfo(ctx, "models/gemini-3.8-flash")
if err != nil {
  log.Fatal(err)
}
fmt.Println("input token limit:", modelInfo.InputTokenLimit)
fmt.Println("output token limit:", modelInfo.OutputTokenLimit)

더 알아보기 (Learn more)

토큰은 Gemini API 비용과 컨텍스트 관리를 이해하는 출발점이에요. thinking 모델의 토큰 사용과 요약은 thinking 문서를, 모델별 컨텍스트 윈도우는 models 문서에서 확인해 보세요. 비용 계산이 궁금하다면 pricing 문서를 이어서 보면 좋아요.