spicyapi문서
본문

텍스트와 스트리밍

OpenAI, Anthropic, Google Gemini의 공식 형식으로 텍스트 모델을 호출하고 대화, 도구, 스트림, 요금을 올바르게 처리합니다.

호출 가능한 텍스트 모델 선택

인증된 /api/v1/models?modality=text&includeSchema=1에서 모델을 읽습니다. enabled와 available이 모두 true인 모델만 요청에 사용하세요. 도구, 멀티모달 메시지, 추론 필드, 구조화 출력은 해당 모델의 inputSchema도 지원해야 쓸 수 있습니다. 프로토콜이 호환된다고 해서 모든 모델이 모든 기능을 갖춘 것은 아닙니다.

같은 모델 ID를 아래 프로토콜 중 어느 것으로든 호출할 수 있으며, 모델을 어느 회사가 만들었는지와는 관계가 없습니다. 다른 회사의 모델을 Anthropic 형식으로, Google이 아닌 모델을 Gemini 형식으로 호출해도 됩니다.

지원 프로토콜

네 가지 텍스트 프로토콜은 같은 API 키와 같은 모델 카탈로그를 쓰고, 레이트 제한·파라미터 검증·과금도 똑같이 적용됩니다. 다른 것은 요청과 응답의 형식뿐입니다. 지금 코드나 SDK에서 이미 쓰고 있는 형식을 고르면 됩니다.

프로토콜엔드포인트키를 넣는 헤더
OpenAI Chat CompletionsPOST /v1/chat/completionsAuthorization: Bearer
OpenAI ResponsesPOST /v1/responsesAuthorization: Bearer
Anthropic MessagesPOST /v1/messagesx-api-key 또는 Authorization: Bearer
Google GeminiPOST /v1beta/models/{model}:generateContentx-goog-api-key 또는 Authorization: Bearer
Google Gemini 스트리밍POST /v1beta/models/{model}:streamGenerateContentx-goog-api-key 또는 Authorization: Bearer
네이티브 스트림POST /api/v1/jobs/streamAuthorization: Bearer

/v1과 /v1beta의 응답과 오류는 각 프로토콜 형식을 따르며, 네이티브 API의 {code,msg,data,request_id} 외피가 붙지 않습니다. body.data만 읽는 클라이언트로 파싱하지 마세요. Gemini 형식은 모델 ID를 URL 경로에 넣으며, ID 안의 슬래시는 그대로 두고 인코딩하지 않아도 됩니다.

키는 요청 헤더에만 넣습니다. x-api-keyx-goog-api-key는 /v1과 /v1beta에서만 유효하며, 네이티브 /api/v1은 Authorization: Bearer만 인식합니다. URL 쿼리 파라미터의 ?key=는 어떤 경우에도 받지 않습니다. 키가 URL에 들어가면 브라우저 기록, 프록시 서버, 액세스 로그에 그대로 남기 때문입니다. 공식 SDK는 모두 요청 헤더로 키를 보내므로 영향이 없습니다.

공식 SDK 연동

베이스 URL과 키만 바꾸고, 나머지는 각 SDK의 평소 사용법을 그대로 따르면 됩니다.

SDK베이스 URL
OpenAI (openai)https://api.spicyapi.ai/v1
Anthropic (anthropic)https://api.spicyapi.ai
Google GenAI (google-genai)https://api.spicyapi.ai

Anthropic과 Google SDK는 /v1 또는 /v1beta를 스스로 붙이므로 베이스 URL에 버전 경로를 넣지 않습니다. OpenAI SDK의 베이스 URL에는 /v1을 포함해야 합니다. 아래 세 가지 Python 예제는 모두 환경 변수에서 SPICY_API_KEY와, 실시간 카탈로그에서 고른 SPICY_MODEL을 읽습니다.

OpenAI
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
reply = client.chat.completions.create(
    model=os.environ["SPICY_MODEL"],
    messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
)
print(reply.choices[0].message.content)
Anthropic
import os
import anthropic

client = anthropic.Anthropic(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai")
message = client.messages.create(
    model=os.environ["SPICY_MODEL"],
    max_tokens=256,
    messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
)
print(message.content[0].text)
Google GenAI
import os
from google import genai
from google.genai import types

client = genai.Client(
    api_key=os.environ["SPICY_API_KEY"],
    http_options=types.HttpOptions(base_url="https://api.spicyapi.ai"),
)
response = client.models.generate_content(
    model=os.environ["SPICY_MODEL"],
    contents="Explain a rainbow in one sentence.",
)
print(response.text)

네트워크 재시도로 요금이 두 번 차감되지 않도록 사용자 동작마다 Idempotency-Key를 하나 만들어 영구 저장하고, SDK의 사용자 지정 헤더 옵션으로 함께 보내세요(OpenAI와 Anthropic SDK에서는 extra_headers).

프로토콜별 파라미터 대응

프로토콜 계층의 필드는 먼저 플랫폼 공통 파라미터 이름으로 바뀐 뒤, 선택한 모델의 inputSchema로 검증됩니다. 모델이 지원하지 않는 파라미터는 400을 반환하며, 조용히 무시된 채 평소대로 과금되는 일은 없습니다. 아래 표는 대응 관계만 설명합니다. ‘—’는 해당 프로토콜에 대응하는 필드가 없다는 뜻입니다. 모델이 어떤 파라미터를 받는지, 값의 범위가 어떤지는 해당 모델의 inputSchema를 따릅니다.

공통 파라미터Chat CompletionsResponsesMessagesGemini
messagesmessagesinstructions, inputsystem, messagessystemInstruction, contents
max_tokensmax_tokens, max_completion_tokensmax_output_tokensmax_tokensgenerationConfig.maxOutputTokens
temperaturetemperaturetemperaturetemperaturegenerationConfig.temperature
top_ptop_ptop_ptop_pgenerationConfig.topP
toolstoolstools (function 타입)tools (사용자 정의 도구)tools[].functionDeclarations
tool_choicetool_choicetool_choicetool_choicetoolConfig.functionCallingConfig
response_formatresponse_formattext.formatgenerationConfig.responseMimeType + responseSchema 또는 responseJsonSchema
reasoning_effortreasoning_effortreasoning.effortgenerationConfig.thinkingConfig

표에 없는 필드도 똑같이 공통 파라미터로 바뀐 뒤 모델 검증을 거치며, inputSchema에 없으면 400을 반환합니다. 예를 들어 모델이 stop을 받지 않으면 Chat의 stop, Messages의 stop_sequences, Gemini의 generationConfig.stopSequences는 모두 400을 반환합니다. 현재는 어느 텍스트 모델도 inputSchema에 seed를 두지 않으므로 Chat의 seed와 Gemini의 generationConfig.seed도 400을 반환합니다.

몇 가지 예외가 있습니다.

  • 해당 프로토콜의 원래 플랫폼에서만 의미가 있는 필드는 무시되며 생성에는 영향이 없습니다. Chat의 user, metadata, store, Responses의 user, metadata, store, include, Messages의 metadata가 여기에 해당합니다.
  • Responses는 previous_response_id를 지원하지 않으므로 input에 전체 이력을 담아 보내야 합니다.
  • Gemini의 safetySettings는 받기는 하지만 적용되지 않습니다. cachedContent, 그리고 googleSearchcodeExecution처럼 서버 측에서 실행되는 도구는 지원하지 않으며 400을 반환합니다. 이미지는 inlineData(base64) 또는 fileData.fileUri(https URL)로 보낼 수 있으며, 모델이 이미지를 받는지는 inputSchema를 따릅니다.

Chat 스트림 요청

서버에서 SPICY_API_KEY, 실시간 카탈로그의 SPICY_MODEL, 업무 작업별로 저장한 SPICY_IDEMPOTENCY_KEY를 설정합니다. 예제에는 jq가 필요합니다. 프롬프트는 바꿀 수 있으며 모든 입력 필드는 현재 Schema를 따라야 합니다.

jq -n --arg model "$SPICY_MODEL" '{
  model: $model,
  messages: [{role: "user", content: "Explain a rainbow in one sentence."}],
  stream: true
}' > chat-request.json

curl --fail-with-body --no-buffer --max-time 120 \
  https://api.spicyapi.ai/v1/chat/completions \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
  --data-binary @chat-request.json

Gemini 요청

환경 변수는 앞 절과 같습니다. 모델 ID는 URL에 쓰고 요청 본문에는 model을 넣지 않습니다. 셸이 뒤따르는 콜론을 변수 수식자로 해석하지 않도록 URL을 큰따옴표로 감싸고 ${SPICY_MODEL} 형태로 쓰세요.

jq -n '{
  systemInstruction: {parts: [{text: "Answer in one sentence."}]},
  contents: [{role: "user", parts: [{text: "Explain a rainbow."}]}],
  generationConfig: {temperature: 0.7, maxOutputTokens: 256}
}' > gemini-request.json

curl --fail-with-body --no-buffer --max-time 120 \
  "https://api.spicyapi.ai/v1beta/models/${SPICY_MODEL}:streamGenerateContent?alt=sse" \
  -H "x-goog-api-key: $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
  --data-binary @gemini-request.json

스트리밍이 필요 없으면 :streamGenerateContent?alt=sse:generateContent로 바꾸고 candidates[0].content.parts[].text에서 답변을 읽습니다. 사용량은 usageMetadata에 있습니다. 스트리밍 요청에 alt=sse를 붙이지 않으면 응답은 조금씩 이어서 전송되는 하나의 JSON 배열이 됩니다. 공식 SDK는 alt=sse를 알아서 붙입니다.

대화와 도구 호출

Chat의 비스트리밍 응답은 choices[0].message를 읽고, 스트리밍은 청크마다 choices[].delta를 읽습니다. TCP 조각 단위로 JSON을 파싱하지 마세요. 도구 인수는 여러 청크로 나뉘어 올 수 있으므로 index별로 조립해 tool_calls를 완성한 뒤 검증·실행하고, assistant 메시지를 보존한 채 해당 tool_call_id로 결과를 돌려보냅니다. Responses는 input에 전체 이력을 보내며 previous_response_id를 지원하지 않습니다. 함수 호출 결과는 function_call_output과 해당 call_id로 반환합니다. Messages는 content 안의 text, tool_use, tool_result 블록을 사용하므로 Chat 메시지 형식을 그대로 적용할 수 없습니다.

Gemini의 contents는 user와 model 두 역할이 번갈아 이어지는 구조입니다. 모델이 도구를 호출할 때는 functionCall 파트를 반환합니다. 도구를 실행한 뒤에는 결과를 functionResponse 파트로 만들어 다음 user 콘텐츠에 담아 돌려보내고, name은 호출 때와 같게 맞춥니다.

종료, 오류, 연결 끊김

프로토콜정상 종료사용량
Chat Completionsdata: [DONE]마지막 청크의 usage
Responsesresponse.completed 이벤트이벤트 안의 response.usage
Messagesmessage_delta 다음에 오는 message_stopmessage_deltausage
GeminifinishReason이 담긴 마지막 이벤트([DONE] 없음)usageMetadata

스트림이 시작되기 전에는 HTTP 상태와 Content-Type을 확인하세요. 오류 본문은 프로토콜마다 고유한 JSON 형식입니다.

프로토콜오류 본문
Chat Completions, Responses{"error": {"message", "type", "param", "code"}}
Messages{"type": "error", "error": {"type", "message"}}
Gemini{"error": {"code", "message", "status"}}

HTTP 상태 코드는 네 프로토콜 모두 같습니다. Gemini의 status에는 Google 상태 이름을 씁니다. 400은 INVALID_ARGUMENT, 401은 UNAUTHENTICATED, 402는 FAILED_PRECONDITION, 403은 PERMISSION_DENIED, 404는 NOT_FOUND, 429는 RESOURCE_EXHAUSTED, 503은 UNAVAILABLE입니다. 프로그램에서 분기할 때는 상태 코드와 오류 유형을 기준으로 하고, message 문구를 비교하지 마세요.

스트림이 시작된 뒤 오류가 나면 Chat과 Gemini는 error 객체가 담긴 이벤트를, Responses는 error 이벤트를, Messages는 event: error를 보냅니다. 클라이언트는 이러한 오류 이벤트와 타임아웃, 조기 연결 종료를 반드시 처리해야 합니다. 프록시 계층에서는 응답 버퍼링을 끄세요. 브라우저를 닫거나 AbortSignal로 중단해도 수신만 멈출 뿐, 작업이 취소되거나 환불이 보장되지는 않습니다. 서버에 신뢰할 수 있는 사용량이 있으면 클라이언트가 먼저 연결을 끊어도 실제로 생성된 사용량만큼 정산합니다. 사용량 정보가 없더라도 연결 종료를 무료 완료로 간주할 수는 없습니다.

견적과 정산

사용자가 정확한 견적과 상한을 먼저 확인하게 하려면 네이티브 jobs/quote → jobs/stream을 사용하고, 같은 model/input과 quoteId, expectedCost를 보내세요. /v1과 /v1beta는 이 네이티브 견적 필드를 받지 않고 접수 시점 가격으로 실행합니다. 최종 비용은 작업 기록과 청구 내역을 기준으로 하며, 텍스트 종료 표시는 정산 근거가 아닙니다. reasoning_tokens는 이미 completion_tokens에 포함되어 있으므로 중복 합산하지 마세요.

다른 요청 형식

다음은 프로토콜 외피 예제입니다. MODEL_ID_FROM_CATALOG를 해당 필드를 지원하는 실제 모델로 바꾸고, 자리표시자를 그대로 보내지 마세요.

POST /v1/responses
{
  "model": "MODEL_ID_FROM_CATALOG",
  "input": "Explain a rainbow in one sentence.",
  "stream": false
}
POST /v1/messages
{
  "model": "MODEL_ID_FROM_CATALOG",
  "max_tokens": 256,
  "messages": [{"role": "user", "content": "Explain a rainbow in one sentence."}],
  "stream": false
}

추가 문서

견적과 프로토콜 호환 · 과금 · 프로덕션 연동 가이드 · 문제 해결과 요청 복구

이 페이지에서