수리 메모를 DeepSeek 모델에 보내고, 무엇을 점검했는지, 무엇을 권고했는지, 고객이 승인했는지, 작업이 끝났는지를 JSON 객체로 돌려받는 서비스가 있다고 해 보겠습니다. 지금은 deepseek-chat, deepseek-flash 또는 OpenRouter 경로를 호출하고 있고, 이를 SpicyAPI(spicyapi.ai)를 통해 DeepSeek V4.1 Flash에서 돌리고 싶은 상황입니다.
바꿀 것은 네 가지입니다. 모델 ID로 deepseek/v4.1-flash/chat을 쓰고, 이 모델이 받지 않는 필드를 모두 삭제하고, reasoning_effort를 고르고, max_tokens에 충분한 여유를 줍니다. 추론은 같은 예산에서 비용이 나가기 때문입니다. 그런 다음 실제 트래픽을 보내기 전에 작은 테스트를 하나 실행하십시오. 요청은 V4.1 Flash 모델 페이지에서 시험해 볼 수 있습니다.
요청에서 바꿀 것
기본 URL, 키, 모델 ID는 쉬운 부분입니다(이전 가이드). 문제가 생기는 곳은 요청 본문입니다.
| 현재 클라이언트 | SpicyAPI |
|---|---|
기본 URL https://api.deepseek.com과 DeepSeek 키 | https://api.spicyapi.ai/v1과 sk-spicy-로 시작하는 키 |
모델 deepseek-flash 또는 이전의 deepseek-chat, deepseek-reasoner, deepseek-v4-flash, deepseek-v4-pro | deepseek/v4.1-flash/chat |
thinking: {"type": "disabled"}(deepseek-chat이 예전에 보이던 동작) | reasoning_effort: "none"이 추론 단계를 건너뜁니다 |
| thinking 켜짐(기본값) | reasoning_effort를 생략하면 high이고, 대량 처리에는 low를 지정합니다 |
reasoning_effort가 max, medium, minimal, xhigh | none, low, high만 허용되며, 그 밖의 값은 400 invalid_request입니다 |
temperature, top_p, presence_penalty, frequency_penalty | 삭제하십시오. 400 unsupported_parameter입니다 |
parallel_tool_calls | 삭제하십시오. 이 모델에서는 노출되지 않습니다 |
stop | 유지합니다. 최대 4개의 시퀀스이며, 일치한 텍스트는 응답에서 빠집니다 |
image_url 또는 파일 파트 | 이미지용 image_url 파트는 유지하고, PDF/파일 파트는 제거한 뒤 먼저 텍스트를 추출합니다 |
OpenRouter의 models, route, provider, usage, transforms, plugins 및 reasoning 객체 | 앞의 여섯 개는 삭제하고, reasoning은 최상위 reasoning_effort로 바꿉니다 |
이 호스팅 모델에는 샘플링 제어가 없으며, reasoning_effort: "none"이어도 마찬가지입니다. 재현성을 temperature: 0에 기대지 말고, 작업 결과 자체를 검증하십시오.
그대로인 것은 다음과 같습니다. messages(텍스트 또는 이미지 콘텐츠, 최대 256개), tools(정의 최대 128개)와 tool_choice(auto, none, required 또는 이름을 지정한 함수 하나), response_format({"type": "json_object"} 사용), stop, max_tokens(기본 32,768, 최대 384,000), 그리고 입력 윈도 1,048,576토큰입니다. 입력 윈도에는 시스템 프롬프트와 이전 도구 결과를 포함해 보내는 모든 메시지가 계산됩니다.
SpicyAPI는 필드를 조용히 버리지 않습니다. 400 오류가 해당 필드를 알려 주고, 요금은 청구되지 않습니다. DeepSeek 자체 API는 thinking 모드에서 temperature와 페널티 필드를 받아들이고 무시하므로, 오래된 클라이언트가 이를 모른 채 들고 있을 수 있습니다.
조용히 실패하는 지점은 모델 이름입니다. DeepSeek 자체 API에서는 V4 Flash가 종료되었고 deepseek-v4-flash가 일시적으로 V4.1 Flash로 연결되며, 현재 이름은 deepseek-flash입니다. V4 Pro는 사정이 다릅니다. DeepSeek은 처음에 9월 14일에 Flash로 전환한다고 발표했지만, 이후 변경 이력에서 V4 Pro 서비스가 요금 변경 없이 계속된다고 확인했습니다. 이전의 Pro 호출이 이미 모두 Flash에서 실행되고 있다고 가정하고 이전하지 마십시오.
SpicyAPI에서는 DeepSeek 채팅 경로 세 개가 각각 별도의 공개 계약을 가집니다. 설정에 deepseek/v4-flash/chat을 그대로 두어도 유효한 요청이지만, 출력 상한은 65,536토큰이고 가격은 더 높습니다. 네이티브 서비스의 별칭은 이 호스팅 계약을 바꾸지 않습니다. 이번 이전에서는 deepseek/v4.1-flash/chat을 명시적으로 사용하십시오.
이 경로는 image_url 파트를 통한 이미지는 받지만 PDF/파일 파트는 받지 않습니다. PDF는 보내기 전에 텍스트를 추출하십시오. 아래의 텍스트 전용 수리 메모 예시에는 이미지 입력이 필요하지 않습니다.
응답에 여유를 주기
모델은 답하기 전에 추론하고, 그 추론은 max_tokens에서 차감됩니다. 기본 effort는 high이며, 상한을 120토큰으로 두었을 때 내용이 전혀 반환되지 않았습니다. 기본 max_tokens는 32,768이고, 이보다 긴 결과물은 잘립니다. 상한은 눈에 보이는 답변에 수천 토큰의 사고 여유를 더한 크기로 잡고, 384,000이라는 최대치에 맞추지 마십시오.
추출은 Low effort로 시작하십시오. 꼭 필요한 구분을 놓친다면 모든 행의 비용을 올리는 대신 해당 레코드에만 High를 시도하십시오. 큰 입력에는 스트리밍 인터페이스를 사용하십시오.
먼저 Playground에서 시험하기
- DeepSeek V4.1 Flash를 열고 Playground를 선택한 다음 effort 컨트롤을 Low로 설정합니다.
- 아래 스크립트의 JSON 형식 지시를 System prompt에, 수리 메모 R17을 Message to the model에 넣고 Run model을 클릭합니다.
- 앱을 이전하기 전에, 승인 대기 상태가 완료된 수리로 바뀌지 않는지 확인합니다.
비싼 실수를 잡아내는 테스트 실행하기
수리 메모는 테스트 소재로 알맞습니다. 매끄러운 요약이 돈이 드는 방식으로 틀릴 수 있기 때문입니다. “교체 권장”은 고객 승인이 아니며, 승인은 완료된 작업이 아닙니다.
API Keys의 키를 SPICY_API_KEY에 설정하고, 이 스크립트를 check_repair.py로 저장해 python3 check_repair.py를 실행하십시오. 시스템 프롬프트에는 JSON의 형태가 나와 있고, DeepSeek의 JSON 모드 가이드가 요구하는 “json”이라는 단어가 들어 있습니다.
import json
import os
import uuid
import urllib.request
import re
SOURCE = (
'Repair note R17: "Inspected the rear brake pads. Recommend replacement before '
'the next long ride. Customer approval is pending." No later work record is supplied.'
)
KEYS = {"inspected", "recommended_action", "authorized", "completed", "evidence"}
REQUIRED_QUOTES = (
"Inspected the rear brake pads.",
"Recommend replacement before the next long ride.",
"Customer approval is pending.",
)
def check(response: dict) -> list[str]:
choice = response["choices"][0]
problems = []
if choice.get("finish_reason") != "stop":
problems.append(f"finish_reason is {choice.get('finish_reason')!r}: the reply may be cut off")
content = (choice.get("message") or {}).get("content")
if not content or not content.strip():
return problems + ["empty content: reasoning may have used the whole max_tokens budget"]
try:
data = json.loads(content)
except json.JSONDecodeError as error:
return problems + [f"not valid JSON: {error}"]
if not isinstance(data, dict):
return problems + ["JSON must be an object"]
if missing := KEYS - data.keys():
problems.append(f"missing keys: {sorted(missing)}")
for key in ("inspected", "authorized", "completed"):
if data.get(key) is not None and type(data.get(key)) is not bool:
problems.append(f"{key} must be true, false or null")
if data.get("inspected") is not True:
problems.append("inspected should be true")
if data.get("authorized") is not False:
problems.append("authorized must be false: approval is pending")
if data.get("completed") is not None:
problems.append("completed must be null: no completion record is supplied")
action = data.get("recommended_action")
if not isinstance(action, str) or not action.strip():
problems.append("recommended_action must be a non-empty string")
else:
words = set(re.findall(r"[a-z]+", action.lower()))
# A lexical screen, not a proof of semantic equivalence.
required = {"rear", "long", "ride"}
compatible = (required <= words
and bool(words & {"replace", "replacement", "replacing"})
and bool(words & {"pad", "pads"})
and not words & {"not", "no", "never", "front", "after"})
if not compatible:
problems.append("review recommendation wording against R17")
quotes = data.get("evidence")
if not isinstance(quotes, list) or not quotes:
problems.append("evidence must be a non-empty list of quotes")
else:
problems += [f"invalid or unmatched quote: {q!r}" for q in quotes
if not isinstance(q, str) or not q.strip() or q not in SOURCE]
for expected in REQUIRED_QUOTES:
if not any(isinstance(q, str) and expected in q for q in quotes):
problems.append(f"missing supporting quote: {expected}")
return problems
body = {'model': 'deepseek/v4.1-flash/chat',
'messages': [{'role': 'system',
'content': 'Extract only facts stated in the source. Reply with one '
'JSON object shaped like {"inspected": null, '
'"recommended_action": null, "authorized": null, '
'"completed": null, "evidence": []}. inspected, '
'authorized and completed are true, false or null; use '
'null for anything the source does not state. evidence '
'lists exact quotes from the source that support every '
'non-null value.'},
{'role': 'user',
'content': 'Repair note R17: "Inspected the rear brake pads. '
'Recommend replacement before the next long ride. '
'Customer approval is pending." No later work record is '
'supplied.'}],
'max_tokens': 2048,
'reasoning_effort': 'low',
'response_format': {'type': 'json_object'}}
request = urllib.request.Request(
"https://api.spicyapi.ai/v1/chat/completions",
data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + os.environ["SPICY_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4())},
)
with urllib.request.urlopen(request, timeout=90) as reply:
response = json.load(reply)
problems = check(response)
print(json.dumps(response.get("usage", {})))
if problems:
raise RuntimeError("; ".join(problems))
print(response["choices"][0]["message"]["content"])기대하는 판단은 점검 완료, 승인은 아직 대기, 완료 여부는 알 수 없음입니다. 권고에는 뒷바퀴 브레이크 패드와 “다음 장거리 라이드 전”이라는 기한이 그대로 담겨야 합니다.
승인과 완료를 별도 테스트로 추가하기
승인 대기 사례에서 멈추지 마십시오. 아래를 추가 테스트의 템플릿으로 쓰고, 각각을 실행하기 전에 SOURCE, REQUIRED_QUOTES, 승인/완료의 기대값을 갱신하십시오. R17용 검사기는 R18이나 R19에 그대로 실행할 수 없습니다.
| 최신 기록 | Authorized | Completed |
|---|---|---|
| R17: “Customer approval is pending.” 이후 기록 없음 | false | null |
| R18: “Customer approved the quoted replacement. Parts ordered; work scheduled for Thursday.” | true | null |
| R19: “Replaced pads; road check completed Thursday.” 승인된 작업에 연결됨 | true | true |
작업 ID, 기록 순서, 날짜는 원본과 함께 보관하십시오. R18은 승인 대기를 대체하지만, 작업이 예약되었다는 사실이 완료를 입증하지는 않습니다. 날짜 없는 “완료” 메모를 같은 작업에 연결할 수 없다면, R17에 합치지 말고 연결이 해결되지 않았다는 점을 그대로 반환하십시오.
같은 세 단계 구분은 크리에이터의 제작 워크플로에서도 나타납니다. “이 캐릭터 디자인을 승인해 주세요”, “디자인 승인됨”, “승인된 디자인으로 영상 생성함”은 각각 요청, 결정, 실행입니다. 추출 프롬프트가 이 단계들을 각각을 뒷받침하는 기록과 함께 따로 출력하게 하고, 메모들이 서로 충돌하면 그 충돌을 그대로 남기십시오. 가장 자신 있게 들리는 문장을 모델이 고르게 해서는 안 됩니다.
도구를 쓰는 워크플로에서도 승인과 완료 분리하기
승인 기록은 수리가 이루어졌다는 증거가 아닙니다. 상태를 바꾸기 전에 완료 기록을 조회하고, 도구가 반환하는 내용에서도 승인과 완료를 별도 필드로 유지하십시오.
도구가 없는 채팅에는 추가로 할 일이 없습니다. 요청에 tools가 있을 때 어시스턴트 메시지를 역할과 내용만으로 다시 만드는 클라이언트는 첫 턴은 통과하지만 두 번째 턴에서 400으로 실패합니다. 그래서 한 턴짜리 테스트로는 이 버그가 드러나지 않습니다. 어시스턴트 메시지는 reasoning_content를 포함해 반환된 그대로 추가한 뒤, 호출마다 tool 메시지를 하나씩, 일치하는 tool_call_id와 함께 추가하고 라운드 수에는 상한을 두십시오. 한 턴이 아니라 두 턴을 테스트하십시오. 채팅 레시피에 전체 루프가 나와 있습니다.
Agent에게 호출을 준비시키기
레코드 추출을 반복한다면 코딩 Agent에게 SpicyAPI Skill을 사용하도록 요청하십시오. “Read deepseek/v4.1-flash/chat, then adapt the example to my input; show the request before running it.” CLI로 조회하는 명령은 spicyapi models get deepseek/v4.1-flash/chat --json이고, MCP에서는 spicyapi_model_get에 model: "deepseek/v4.1-flash/chat"을 지정합니다. 채팅 호출은 호환 클라이언트나 채팅 API를 사용하며, 미디어 작업용 명령은 채팅 응답을 생성하지 않습니다.
비용 다시 계산하기
요금은 캐시되지 않은 입력 100만 토큰당 $0.30, 캐시된 입력 100만 토큰당 $0.006, 출력 100만 토큰당 $1.20이며, 요청 길이와 상관없이 단일 요율입니다. 캐시되지 않은 40,000토큰 프롬프트에 출력 2,000토큰이면 $0.0144입니다. 입력 토큰 중 30,000이 캐시로, 10,000이 캐시 없음으로 청구되면 같은 호출은 $0.00558입니다. 추론 토큰은 출력으로 청구되므로, 눈에 보이는 답변이 짧아도 생각보다 많은 출력이 사용될 수 있습니다. 청구에서 빼 주는 설정은 none뿐입니다.
DeepSeek 자체 API에서 옮길 때는 두 가지 비교가 중요합니다. DeepSeek의 정가는 피크 시간대에 같은 $0.30, $0.006, $1.20이지만 비피크 시간대에는 반값이고, SpicyAPI는 단일 요율입니다. DeepSeek에서 비피크 시간대에 예약해 돌리는 배치는 같은 토큰이라도 SpicyAPI에서는 약 두 배가 듭니다. 또한 캐시 요율은 입력 요율의 50분의 1이지만, 호출마다 바이트 단위로 동일한 프리픽스에만 적용됩니다. 1,500토큰 시스템 프롬프트를 10,000번 호출하면 입력은 1,500만 토큰이고, 캐시 없이는 $4.50이고, 모두 캐시 읽기였다 해도 최대 $0.09입니다. 반환된 usage와 정산된 비용을 이 추정치와 비교하십시오. 타임스탬프나 순서가 바뀐 지시가 공유 프리픽스를 깨뜨릴 수 있으며, 입력이 그대로라는 사실만으로 캐시 적중이 보장되지는 않습니다.
DeepSeek 경로별 비교
| V4.1 Flash | V4 Flash | V4 Pro | |
|---|---|---|---|
| 모델 ID | deepseek/v4.1-flash/chat | deepseek/v4-flash/chat | deepseek/v4-pro/chat |
| 최대 출력 | 384,000 | 65,536 | 384,000 |
| 입력 / 캐시 / 출력(100만 토큰당) | $0.30 / $0.006 / $1.20 | $0.44 / $0.014 / $1.32 | $1.32 / $0.044 / $3.96 |
parallel_tool_calls | 노출되지 않음 | 노출되지 않음 | 노출됨 |
세 모델 모두 입력 1,048,576토큰, effort none, low, high(기본값 high), 기본 max_tokens 32,768을 받습니다. deepseek/v4-flash/chat에서 옮길 때는 요청 본문이 동일하므로 ID만 바뀌고, 출력 상한은 거의 여섯 배로 늘며 모든 요율이 내려갑니다. V4 Pro에서 옮길 때는 parallel_tool_calls를 쓸 수 없게 되고, 입력은 77%, 출력은 약 70% 저렴해집니다. 특정 작업에 V4 Flash와 V4 Pro 중 무엇이 더 맞는지는 별개의 결정이며, DeepSeek V4 Flash vs Pro에서 다룹니다.
앱이 성인 대상 픽션을 쓴다면
SpicyAPI에서 V4.1 Flash는 무검열로 표기되어 있지만, 성인 대상 픽션 요청을 모두 완성해 준다는 약속은 아닙니다. 2026년 9월 테스트에서 강한 성인 장면을 직접 요청하면 매번 순화되어 돌아왔고, 픽션으로 틀을 짜거나 시스템 프롬프트에서 그 틀을 설정하면 더 많은 장면이 그대로 쓰였지만 전부는 아니었습니다(자세한 내용은 V4.1 Flash 리뷰 참조). 직접적인 요청을 받아 주던 모델에 맞춰 프롬프트를 썼다면, 이전하면서 그 틀을 시스템 메시지로 옮기고 모든 등장인물을 성인으로 유지하십시오.
이전 중 실패를 바로 고치기
| 증상 | 가능한 원인 | 해결 |
|---|---|---|
400 unsupported_parameter가 temperature, top_p, 페널티, parallel_tool_calls 또는 thinking을 지목함 | 이전 클라이언트에서 넘어온 필드 | 삭제 |
content가 비어 있거나 finish_reason이 length임 | max_tokens를 추론에 다 썼거나 응답이 이를 넘음 | max_tokens를 높이고 effort를 낮춘 뒤 재시도 |
| JSON은 파싱되지만 완료된 작업으로 보고함 | 권고를 완료로 해석함 | 필드를 분리하고 인용을 요구하며 검사기를 실행 |
| 도구 대화의 두 번째 턴에서 오류 | 어시스턴트 메시지를 reasoning_content 없이 다시 구성함 | 반환된 메시지를 그대로 추가 |
| 잔액이 충분한데 402 | 40201 홀드가 잔액을 넘었거나 40202 지출 한도에 도달함 | 오류를 확인하십시오. 홀드를 위해 과도한 출력 예산을 줄이거나 키/워크스페이스 한도를 점검 |
재사용한 Idempotency-Key가 거부됨(400 또는 409) | 키가 완료된 요청이나 다른 본문에 속함 | 완료된 요청에 대한 400은 응답을 재현할 수 없고, 409는 본문이 바뀌었음을 뜻할 수 있습니다. 키는 유효 기간 안의 동일한 네트워크 재시도에만 재사용하고, 새 유료 호출을 고르기 전에 작업의 정산 내역을 확인하십시오 |
| 스트리밍하지 않은 긴 호출이 약 100초 근처에서 끊김 | 네트워크 엣지에서의 차단 | 응답을 스트리밍 |
기존 기준선을 잃지 않고 전환하기
설정에는 이전 모델 ID를 남겨 두고 두 쪽 모두에서 테스트를 실행하십시오. 새 ID에서 테스트가 통과하고, 자체 소스가 검증되며, 기록한 usage가 위의 계산과 일치하면 트래픽을 옮기십시오.








