OpenAI·Claude·Gemini API 예제: 스트리밍·JSON·도구 호출 TypeScript
세 API는 스트리밍 이벤트, 구조화 출력과 도구 결과를 돌려주는 방식이 다릅니다. 현재 공식 TypeScript SDK에서 컴파일되는 예제 10개로 차이를 확인했습니다.
AI는 자료 조사와 초안 정리에 보조적으로 사용했으며, 편집부가 출처와 사실을 확인했습니다.
OpenAI Claude Gemini API 예제를 한 파일에서 바꿔 끼우려 하면 메서드 이름보다 응답 구조에서 먼저 막힌다. 스트리밍 조각, 구조화 출력, 도구 호출 결과를 돌려주는 방식이 세 API에서 서로 다르기 때문이다.
2026년 8월 18일 공식 문서와 공개 TypeScript SDK를 다시 확인했다. 아래 코드는 [email protected], @anthropic-ai/[email protected], @google/[email protected], [email protected] 조합으로 타입 검사를 거쳤다. 유료 API 요청은 실행하지 않아 실제 응답과 과금 결과까지 시험하지는 않았다.
OpenAI Claude Gemini API 예제에서 바뀌는 지점
| 작업 | OpenAI | Claude | Gemini |
|---|---|---|---|
| 기본 호출면 | Responses API | Messages API | Generate Content API |
| 텍스트 스트리밍 | response.output_text.delta 이벤트 | messages.stream()의 text 이벤트 | generateContentStream()의 chunk.text |
| 구조화 출력 | responses.parse()와 zodTextFormat() | messages.parse()와 zodOutputFormat() | JSON Schema를 보내고 response.text를 직접 검증 |
| 도구 요청 위치 | response.output의 function_call | message.content의 tool_use | response.functionCalls |
| 실행 결과 반환 | function_call_output과 call_id | 사용자 메시지의 tool_result와 tool_use_id | 사용자 Content의 functionResponse와 id |
세 기능은 맡는 일이 다르다. 스트리밍은 첫 조각을 받을 때까지의 대기를 줄인다. 구조화 출력은 최종 답변의 모양을 고정한다. 도구 호출은 모델이 외부 작업을 요청하는 프로토콜이라 애플리케이션이 함수를 실행한 뒤 결과를 다시 보내야 끝난다.
Google에는 Interactions API도 있지만 공식 TypeScript SDK 저장소는 이 기능을 아직 experimental로 표시한다. 여기서는 현재 공개 SDK 타입과 맞는 models.generateContent* 계열을 사용했다.
실행 환경부터 맞춘다
세 SDK를 한 프로젝트에서 시험하려면 Node.js 22를 쓰는 편이 안전하다. 2026년 8월 18일 공개된 OpenAI SDK의 최소 Node.js 버전이 22이기 때문이다.
npm install openai @anthropic-ai/sdk @google/genai zod
npm install --save-dev typescript tsx @types/node
npm pkg set type=module
예제는 ESM과 최상위 await를 쓴다. 기존 프로젝트에서 모듈 방식을 바꾸기 어렵다면 코드를 프로젝트의 비동기 함수 안으로 옮긴다.
모델 이름을 코드에 박아 두면 모델 교체나 접근 권한 차이 때문에 예제가 금방 깨진다. 각 계정에서 호출 가능한 모델 ID를 환경 변수로 넘긴다.
OPENAI_API_KEY=...
OPENAI_MODEL=...
ANTHROPIC_API_KEY=...
ANTHROPIC_MODEL=...
GEMINI_API_KEY=...
GEMINI_MODEL=...
아래 예제는 브라우저가 아니라 서버에서 실행해야 한다. 세 회사의 비밀 키를 클라이언트 번들에 넣으면 방문자가 그대로 꺼내 쓴다.
스트리밍은 완성된 문자열이 아니라 조각을 받는다
OpenAI는 이벤트 종류를 골라 읽는다
OpenAI 스트리밍 문서는 Responses API가 SSE 이벤트를 보낸다고 설명한다. 텍스트만 화면에 붙일 때는 모든 이벤트를 출력하지 말고 response.output_text.delta만 읽는다.
import OpenAI from "openai";
const model = process.env.OPENAI_MODEL;
if (!model) throw new Error("OPENAI_MODEL is required");
const openai = new OpenAI();
const stream = await openai.responses.create({
model,
input: "429 오류가 생기는 이유를 두 문장으로 설명해 줘.",
stream: true,
});
for await (const event of stream) {
if (event.type === "response.output_text.delta") {
process.stdout.write(event.delta);
}
}
도구 인수는 response.function_call_arguments.delta, 거절문은 response.refusal.delta처럼 다른 이벤트로 온다. 텍스트 델타만 모아 놓고 전체 응답이라고 가정하면 이 상태를 놓친다.
Claude는 스트림을 읽은 뒤 최종 메시지를 확인한다
Claude 스트리밍 문서는 messages.stream()과 finalMessage() 조합을 안내한다. text 이벤트로 화면을 갱신한다. 마지막에는 누적된 메시지의 stop_reason을 확인한다.
import Anthropic from "@anthropic-ai/sdk";
const model = process.env.ANTHROPIC_MODEL;
if (!model) throw new Error("ANTHROPIC_MODEL is required");
const anthropic = new Anthropic();
const stream = anthropic.messages.stream({
model,
max_tokens: 512,
messages: [
{ role: "user", content: "429 오류가 생기는 이유를 두 문장으로 설명해 줘." },
],
});
stream.on("text", (text) => process.stdout.write(text));
const message = await stream.finalMessage();
if (message.stop_reason !== "end_turn") {
console.error(`stop_reason=${message.stop_reason}`);
}
max_tokens나 tool_use로 끝난 응답은 정상적인 대화 종료와 다르다. 화면에 글자가 보였다는 이유만으로 완료 처리하면 JSON이나 도구 호출이 잘린 채 남는다.
Gemini는 응답 청크의 text를 순서대로 붙인다
Gemini 텍스트 생성 문서는 generateContentStream()이 GenerateContentResponse 청크를 비동기 반복자로 돌려준다고 설명한다.
import { GoogleGenAI } from "@google/genai";
const model = process.env.GEMINI_MODEL;
if (!model) throw new Error("GEMINI_MODEL is required");
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) throw new Error("GEMINI_API_KEY is required");
const gemini = new GoogleGenAI({ apiKey });
const stream = await gemini.models.generateContentStream({
model,
contents: "429 오류가 생기는 이유를 두 문장으로 설명해 줘.",
});
for await (const chunk of stream) {
process.stdout.write(chunk.text ?? "");
}
세 예제 모두 이미 내보낸 조각을 따로 보관해야 한다. 연결이 끊긴 뒤 요청 전체를 다시 보내면 앞부분이 중복될 수 있으므로, 스트림 재시도와 일반 HTTP 재시도를 같은 문제로 다루면 안 된다.
구조화 출력은 마지막에 한 번 더 검증한다
세 예제는 같은 고객 문의를 priority, summary 두 필드로 바꾼다. 스키마를 지켰다는 사실은 값이 업무 규칙에 맞는다는 보증이 아니다. 저장 전에 Zod로 최종 검증한다. 거절이나 출력 한도 종료도 별도로 처리한다.
OpenAI 구조화 출력
OpenAI 구조화 출력 문서는 TypeScript에서 responses.parse()와 zodTextFormat()을 제공한다.
import OpenAI from "openai";
import { zodTextFormat } from "openai/helpers/zod";
import { z } from "zod";
const Ticket = z.object({
priority: z.enum(["low", "normal", "high"]),
summary: z.string(),
});
const model = process.env.OPENAI_MODEL;
if (!model) throw new Error("OPENAI_MODEL is required");
const openai = new OpenAI();
const response = await openai.responses.parse({
model,
input: "결제가 두 번 됐습니다. 오늘 안에 확인해 주세요.",
text: { format: zodTextFormat(Ticket, "support_ticket") },
});
if (!response.output_parsed) throw new Error("Structured output was not produced");
const ticket = Ticket.parse(response.output_parsed);
console.log(ticket);
Claude 구조화 출력
Claude 구조화 출력 문서는 현재 필드가 output_config.format이라고 명시한다. 과거 베타 예제의 output_format을 그대로 복사하면 새 코드와 섞인다.
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const Ticket = z.object({
priority: z.enum(["low", "normal", "high"]),
summary: z.string(),
});
const model = process.env.ANTHROPIC_MODEL;
if (!model) throw new Error("ANTHROPIC_MODEL is required");
const anthropic = new Anthropic();
const message = await anthropic.messages.parse({
model,
max_tokens: 512,
messages: [
{ role: "user", content: "결제가 두 번 됐습니다. 오늘 안에 확인해 주세요." },
],
output_config: { format: zodOutputFormat(Ticket) },
});
if (!message.parsed_output) throw new Error(`stop_reason=${message.stop_reason}`);
const ticket = Ticket.parse(message.parsed_output);
console.log(ticket);
Gemini 구조화 출력
Gemini의 공개 SDK는 이 경로에서 파싱된 객체를 돌려주지 않는다. Gemini 구조화 출력 문서가 설명하는 JSON Schema를 보낸 뒤 돌아온 문자열을 애플리케이션에서 파싱한다.
import { GoogleGenAI } from "@google/genai";
import { z } from "zod";
const Ticket = z.object({
priority: z.enum(["low", "normal", "high"]),
summary: z.string(),
});
const ticketJsonSchema = {
type: "object",
properties: {
priority: { type: "string", enum: ["low", "normal", "high"] },
summary: { type: "string" },
},
required: ["priority", "summary"],
additionalProperties: false,
};
const model = process.env.GEMINI_MODEL;
if (!model) throw new Error("GEMINI_MODEL is required");
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) throw new Error("GEMINI_API_KEY is required");
const gemini = new GoogleGenAI({ apiKey });
const response = await gemini.models.generateContent({
model,
contents: "결제가 두 번 됐습니다. 오늘 안에 확인해 주세요.",
config: {
responseMimeType: "application/json",
responseJsonSchema: ticketJsonSchema,
},
});
if (!response.text) throw new Error("Structured output was not produced");
const ticket = Ticket.parse(JSON.parse(response.text));
console.log(ticket);
2026년 8월 18일 공개 문서에는 새 responseFormat 예제도 보이지만 @google/[email protected]의 GenerateContentConfig 타입은 responseMimeType과 responseJsonSchema를 제공한다. 위 코드는 공개 패키지와 타입이 일치하는 쪽을 택했다. SDK를 올릴 때 이 두 필드를 먼저 다시 확인해야 한다.
도구 호출은 요청과 실행 결과를 한 쌍으로 돌려준다
아래 코드는 모두 조회만 하는 get_order_status 모의 함수를 쓴다. 환불이나 결제처럼 부작용이 있는 함수라면 호출 ID와 주문 ID를 멱등성 키로 저장한 뒤 실행해야 한다.
OpenAI는 previous_response_id로 응답 이력을 잇는다
OpenAI 함수 호출 문서는 function_call의 call_id와 function_call_output을 연결한다.
reasoning 모델에서는 도구 호출 외의 reasoning 항목도 다음 요청에 필요하다. previous_response_id를 넘기면 출력 항목을 직접 재구성하지 않고 앞 응답을 잇는다.
import OpenAI from "openai";
import { z } from "zod";
const OrderInput = z.object({ order_id: z.string() });
const getOrderStatus = (orderId: string) => ({
order_id: orderId,
status: "shipped",
});
const model = process.env.OPENAI_MODEL;
if (!model) throw new Error("OPENAI_MODEL is required");
const openai = new OpenAI();
const tools: OpenAI.Responses.Tool[] = [{
type: "function",
name: "get_order_status",
description: "Look up an order status by order ID.",
parameters: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
additionalProperties: false,
},
strict: true,
}];
const response = await openai.responses.create({
model,
input: "주문 A-104의 배송 상태를 알려 줘.",
tools,
});
const toolOutputs: OpenAI.Responses.ResponseInput = [];
for (const item of response.output) {
if (item.type !== "function_call" || item.name !== "get_order_status") continue;
const args = OrderInput.parse(JSON.parse(item.arguments));
toolOutputs.push({
type: "function_call_output",
call_id: item.call_id,
output: JSON.stringify(getOrderStatus(args.order_id)),
});
}
if (toolOutputs.length === 0) throw new Error("Tool call was not produced");
const final = await openai.responses.create({
model,
previous_response_id: response.id,
input: toolOutputs,
tools,
});
console.log(final.output_text);
Claude는 tool_use 바로 다음에 tool_result를 놓는다
Claude 도구 결과 문서는 tool_result가 대응하는 tool_use 직후 메시지에 와야 한다고 설명한다. 결과 블록보다 일반 텍스트를 먼저 넣어도 400 오류가 난다.
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const OrderInput = z.object({ order_id: z.string() });
const getOrderStatus = (orderId: string) => ({
order_id: orderId,
status: "shipped",
});
const model = process.env.ANTHROPIC_MODEL;
if (!model) throw new Error("ANTHROPIC_MODEL is required");
const anthropic = new Anthropic();
const tools: Anthropic.Tool[] = [{
name: "get_order_status",
description: "Look up an order status by order ID.",
strict: true,
input_schema: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
additionalProperties: false,
},
}];
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "주문 A-104의 배송 상태를 알려 줘." },
];
const first = await anthropic.messages.create({
model,
max_tokens: 512,
messages,
tools,
});
messages.push({ role: "assistant", content: first.content });
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of first.content) {
if (block.type !== "tool_use" || block.name !== "get_order_status") continue;
const args = OrderInput.parse(block.input);
results.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(getOrderStatus(args.order_id)),
});
}
if (results.length === 0) throw new Error(`stop_reason=${first.stop_reason}`);
messages.push({ role: "user", content: results });
const final = await anthropic.messages.create({
model,
max_tokens: 512,
messages,
tools,
});
console.log(
final.content.filter((block) => block.type === "text").map((block) => block.text).join(""),
);
Gemini는 모델이 돌려준 Content를 다시 넣는다
Gemini 함수 호출 문서는 모델의 함수 호출 Content와 실행 결과 functionResponse를 대화 기록에 차례로 넣는다.
Gemini 3 계열의 thought signature도 원래 모델 Content에 붙는다. 함수 호출 객체를 새로 만들지 말고 응답의 content를 그대로 보존한다.
import { GoogleGenAI, type Content, type FunctionDeclaration } from "@google/genai";
import { z } from "zod";
const OrderInput = z.object({ order_id: z.string() });
const getOrderStatus = (orderId: string) => ({
order_id: orderId,
status: "shipped",
});
const model = process.env.GEMINI_MODEL;
if (!model) throw new Error("GEMINI_MODEL is required");
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) throw new Error("GEMINI_API_KEY is required");
const declaration: FunctionDeclaration = {
name: "get_order_status",
description: "Look up an order status by order ID.",
parametersJsonSchema: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
additionalProperties: false,
},
};
const config = { tools: [{ functionDeclarations: [declaration] }] };
const contents: Content[] = [
{ role: "user", parts: [{ text: "주문 A-104의 배송 상태를 알려 줘." }] },
];
const gemini = new GoogleGenAI({ apiKey });
const first = await gemini.models.generateContent({ model, contents, config });
const call = first.functionCalls?.find((item) => item.name === "get_order_status");
const modelContent = first.candidates?.[0]?.content;
if (!call?.name || !modelContent) throw new Error("Tool call was not produced");
const args = OrderInput.parse(call.args);
contents.push(modelContent);
contents.push({
role: "user",
parts: [{
functionResponse: {
name: call.name,
response: { output: getOrderStatus(args.order_id) },
...(call.id ? { id: call.id } : {}),
},
}],
});
const final = await gemini.models.generateContent({ model, contents, config });
console.log(final.text);
id는 SDK 타입에서 선택값이다. 응답에 들어 있을 때만 functionResponse에 돌려준다. 이 예제는 첫 get_order_status 호출 하나만 처리한다. 실제 응답에는 도구 호출이 없거나 여러 개가 함께 오기도 하므로 운영 코드에서는 배열 전체를 순회해야 한다. 모르는 도구 이름은 실행하지 않으며 입력은 함수에 넘기기 전에 다시 검증해야 한다.
429와 재시도는 SDK 설정을 먼저 확인한다
OpenAI와 Anthropic의 공식 TypeScript SDK는 연결 오류, 408, 409, 429, 5xx처럼 일시적인 오류를 기본 두 번 재시도한다. maxRetries는 최초 요청 뒤의 재시도 횟수다.
Gemini의 429는 RESOURCE_EXHAUSTED이며 할당량은 API 키가 아니라 프로젝트 단위로 적용된다. @google/genai의 retryOptions.attempts는 최초 요청을 포함한다. 아래처럼 3으로 두면 최대 두 번 더 시도한다.
import OpenAI from "openai";
import Anthropic from "@anthropic-ai/sdk";
import { GoogleGenAI } from "@google/genai";
const openai = new OpenAI({ maxRetries: 2, timeout: 30_000 });
const anthropic = new Anthropic({ maxRetries: 2, timeout: 30_000 });
const gemini = new GoogleGenAI({
apiKey: process.env.GEMINI_API_KEY,
httpOptions: {
timeout: 30_000,
retryOptions: {
attempts: 3,
initialDelay: 1,
maxDelay: 8,
expBase: 2,
jitter: 1,
},
},
});
void [openai, anthropic, gemini];
OpenAI 한도 문서, Claude 한도 문서, Gemini 한도 문서를 보면 429의 계산 단위도 다르다.
재시도 코드를 늘리기 전에 계정의 RPM·입력 토큰·출력 토큰·일일 한도 중 무엇이 찼는지 확인해야 한다.
SDK 재시도 바깥에 또 무한 반복문을 두면 한 번의 사용자 요청이 예상보다 많이 호출된다. 재시도 횟수와 전체 제한 시간을 함께 기록한다. 스트림 중단이나 결제·할당량 오류는 일반 429 재시도와 분리한다.
장애 격리, fallback, 로그는 LLM API 장애 대응 가이드에서 이어 다룬다.
복사한 뒤 반드시 바꿀 부분
- 환경 변수의 모델 ID는 각 계정에서 실제 접근 가능한 값으로 넣는다.
- 모의
getOrderStatus()를 실제 함수로 바꿀 때 인증과 권한 검사를 함수 안에서 다시 한다. - 구조화 출력은 스키마 통과 뒤에도 주문 ID 존재 여부나 허용 상태 같은 업무 규칙을 검증한다.
- 도구 호출이 여러 개면 모든 호출 ID에 결과를 돌려준다. 부작용이 있는 작업은 멱등성 키를 저장한다.
- SDK를 올리면 타입 검사와 최소 한 번의 샌드박스 호출을 다시 실행한다. 특히 Gemini의 구조화 출력 필드는 문서와 공개 패키지의 전환이 진행 중이다.
이 글의 코드는 2026년 8월 18일 공개 패키지에서 컴파일만 확인했다. 실제 API 응답, 모델별 구조화 출력 지원, 계정별 한도는 호출하지 못했으므로 운영 배포 전 각 회사의 테스트 프로젝트에서 다시 확인해야 한다.
예제에 넣은 모델 ID의 교체 시점은 AI API 지원 종료 일정에서 따로 추적한다. SDK가 컴파일돼도 종료된 모델은 호출되지 않는다.
참고한 출처
공식 발표·문서·changelog 기반으로 작성했습니다. 전체 18개 중 공식 출처는 18개입니다.
- Streaming API responses(새 창)공식OpenAI
- Structured model outputs(새 창)공식OpenAI
- Function calling(새 창)공식OpenAI
- OpenAI API rate limits(새 창)공식OpenAI
- OpenAI Node.js SDK(새 창)공식OpenAI
- Streaming messages(새 창)공식Anthropic
- Claude structured outputs(새 창)공식Anthropic
- Handle tool calls(새 창)공식Anthropic
- Claude API rate limits(새 창)공식Anthropic
- Claude TypeScript SDK(새 창)공식Anthropic
- Anthropic TypeScript SDK helpers(새 창)공식Anthropic
- Gemini text generation(새 창)공식Google
- Gemini structured outputs(새 창)공식Google
- Gemini function calling(새 창)공식Google
- Gemini API rate limits(새 창)공식Google
- Google Gen AI JavaScript SDK(새 창)공식Google
- Google Gen AI HTTP implementation(새 창)공식Google
- Google Gen AI TypeScript types(새 창)공식Google