블로그로 돌아가기

API & SDK가이드

API로 하나의 프롬프트에서 HTML 도구 생성하기 (Python)

하나의 프롬프트를 동작하는 tool.html로 바꾸고, finish_reason으로 잘린 답변을 잡아내며, 토큰과 비용을 출력하는 Python 스크립트입니다.

작성자: CheapTokens Team 게시됨읽는 시간 7분

finish_reason을 검사하고 'One prompt, one HTML tool' 제목 옆에 tool.html을 저장하는 코드 스니펫
이 페이지의 내용
  1. "도구를 만들어 줘" 프롬프트에 필요한 것
  2. 스크립트: 요청, 검사, 저장
  3. max_tokens가 페이지 동작 여부를 결정하는 이유
  4. 열어서 페이지 확인하기
  5. 한 번 생성하는 데 드는 비용
  6. 동작하지 않을 때

한 번의 API 호출로 바로 쓸 수 있는 단일 파일 도구를 얻을 수 있습니다. 잘 구조화된 프롬프트를 보내고, max_tokens가 페이지 전체를 담을 만큼 충분한지 확인하고, finish_reason이 stop인지 검사한 뒤 응답을 tool.html로 저장하면 됩니다. 아래에는 정확히 그 일을 하는 스크립트와, 자신의 도구에 맞게 고쳐 쓸 수 있는 프롬프트 템플릿이 있습니다.

아이디어는 단순하며, DeepSeek이 방금 이를 보여주었습니다. DeepSeek Harness의 Creator Mode용 데모 프롬프트 일곱 개를 공개했습니다(해당 페이지, 2026년 11월 3일 확인): 3D 랜드마크 갤러리, 포스터 작업대, 영상 편집 작업대, 논문 리더, 대화 통계 보드 등입니다. Creator Mode는 Harness 앱 안에 있습니다. 같은 "프롬프트 입력, 도구 출력" 흐름을 자신의 스크립트와 자신의 키로 쓰고 싶다면, 단일 파일의 경우에는 평범한 채팅 완성이면 충분합니다. Harness 자체를 쓴다면 CheapTokens에 연결하기는 별도의 짧은 가이드입니다.

"도구를 만들어 줘" 프롬프트에 필요한 것#

그 데모 프롬프트 몇 개를 읽어 보면 패턴이 보입니다. 각 프롬프트는 도구가 무엇인지, 화면에 무엇이 있는지, 컨트롤이 무엇을 하는지, 시작 상태는 무엇인지, 그리고 무엇을 해서는 안 되는지를 말합니다. 이 중 하나라도 빠지면 모델이 그 빈틈을 자기 추측으로 채우는데, 그 추측은 보통 당신의 의도와 다릅니다.

다음은 다섯 부분으로 된 템플릿입니다:

text
What it is: <one sentence: the tool and who uses it>
Layout: <what is on the screen, top to bottom>
Controls: <every input and button, and what each one does>
Defaults: <what the page shows before the user touches anything>
Constraints: <single HTML file, no external requests, no libraries, size of the page>

그리고 채워 넣은 버전입니다. 요리 단위를 계속 변환하면서 변환기를 검색하는 데 지쳤다고 해 봅시다:

text
What it is: a unit converter for cooking, for someone standing in a kitchen with a phone.
Layout: a title, one number field, two dropdowns (from, to), a large result line under them.
Controls: the dropdowns hold cup, tablespoon, teaspoon, ml, oz, g (flour) and g (sugar). Typing a number updates the result immediately. A swap button exchanges the two dropdowns.
Defaults: 1 cup to ml, result already shown.
Constraints: one HTML file with inline CSS and JavaScript, no external libraries, fonts or network requests, works on a narrow phone screen, dark text on a light background.

이것을 스크립트 옆에 prompt.txt로 저장하세요. "Constraints" 줄이 가장 큰 비중을 차지합니다: "no external requests"가 없으면 모델이 CDN 라이브러리를 끌어오려 하고, 그러면 페이지가 오프라인에서 깨집니다.

전과 후: 한 줄 프롬프트와 다섯 부분으로 된 같은 프롬프트
같은 요청을 한 줄로 쓴 것과 다섯 부분으로 쓴 것: 무엇인지, 레이아웃, 컨트롤, 기본값, 제약

스크립트: 요청, 검사, 저장#

CheapTokens API는 https://api.cheaptokens.dev/v1에서 OpenAI Chat Completions와 호환되므로, openai 패키지는 두 가지만 바꾸면 동작합니다: base URL과 키입니다. 페이지 전체를 위해서는 deepseek-ai/DeepSeek-V4-Flash-0731를 사용합니다: 답변당 최대 32,768 출력 토큰을 허용하고 380K 컨텍스트 창을 가집니다. 전체 API 세부 사항은 CheapTokens 문서에 있습니다.

패키지를 설치하고, 키를 환경 변수에 넣고, 가격표에서 두 가격을 설정하세요(스크립트는 이를 사용해 비용을 출력합니다):

bash
pip install openai
export CHEAPTOKENS_API_KEY="your-key"
export PRICE_IN_PER_M="<input price per 1M from the table>"
export PRICE_OUT_PER_M="<output price per 1M from the table>"

그다음 make_tool.py:

python
import os
import re
import sys

from openai import OpenAI

MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'
PRICE_IN = float(os.environ['PRICE_IN_PER_M'])    # USD per 1M input tokens (from the table)
PRICE_OUT = float(os.environ['PRICE_OUT_PER_M'])  # USD per 1M output tokens (from the table)
FENCE = '`' * 3

client = OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)

prompt = open('prompt.txt', encoding='utf-8').read()

resp = client.chat.completions.create(
    model=MODEL,
    max_tokens=16000,
    messages=[
        {'role': 'system', 'content': 'You write one self-contained HTML file. Reply with the file only, no commentary.'},
        {'role': 'user', 'content': prompt},
    ],
)

choice = resp.choices[0]
if choice.finish_reason == 'length':
    sys.exit('Cut off at max_tokens: the page is unfinished. Raise max_tokens or shrink the request.')

text = choice.message.content or ''
m = re.search(FENCE + r'(?:html)?\s*(.*?)' + FENCE, text, re.DOTALL)
html = (m.group(1) if m else text).strip()
if '</html>' not in html.lower():
    sys.exit('The reply has no closing </html>; not saving it.')

with open('tool.html', 'w', encoding='utf-8') as f:
    f.write(html)

u = resp.usage
cost = (u.prompt_tokens * PRICE_IN + u.completion_tokens * PRICE_OUT) / 1_000_000
print(f'saved tool.html, {u.prompt_tokens} in / {u.completion_tokens} out, about ${cost:.6f}')

단계의 순서가 중요하며, 틀리기 쉽습니다:

순서대로 배열하기

스크립트의 단계를 실행 순서대로 배치하세요

  1. 넉넉한 max_tokens로 프롬프트를 보낸다
  2. 텍스트를 건드리기 전에 finish_reason을 확인한다
  3. 코드 펜스가 있으면 그 안에서 HTML을 꺼낸다
  4. 닫는 </html>이 있는지 확인한다
  5. tool.html을 저장하고 사용량을 출력한다
파이프라인: 프롬프트, API 호출, finish_reason 검사, tool.html
스크립트가 프롬프트에서 저장된 파일까지 하는 일

max_tokens가 페이지 동작 여부를 결정하는 이유#

완전한 인터랙티브 페이지는 긴 답변입니다. HTML, CSS, JavaScript가 빠르게 쌓이며, 모델은 그 순서대로 작성합니다: 답변이 잘리면 스크립트의 끝부분을 먼저 잃게 되는데, 바로 그 부분이 버튼을 동작하게 만듭니다. 브라우저는 여전히 페이지를 열 수 있습니다. 다만 아무 동작도 하지 않을 뿐입니다.

그래서 스크립트는 텍스트를 보기 전에 finish_reason을 확인합니다. stop는 모델이 스스로 끝냈다는 뜻입니다. length은 파일 중간에 max_tokens 한도에 부딪혔다는 뜻입니다. 이 문제의 일반적인 버전은 AI 답변이 잘렸나요? finish_reason length 수정에서 다룹니다; 여기서 규칙은 짧습니다: finish_reason가 length인 페이지는 절대 저장하지 마세요.

정말로 이것을 보게 된다면:

  • max_tokens을 올리세요(DeepSeek-V4-Flash는 답변당 최대 32,768을 허용합니다);
  • 또는 더 적게 요청하세요: 컨트롤을 줄이고, 화면 하나, 애니메이션 없이;
  • GLM-5.3-Flash(zai-org/GLM-5.3-Flash)에서는 한도가 8,192 출력 토큰이므로, 그 모델은 위의 변환기 같은 작은 도구에 적합합니다.

두 번째 검사인 텍스트 내 </html>은 모델이 파일을 끝내지 않고 정중하게 답변을 마치는 더 조용한 실패를 잡아냅니다. max_tokens을 설정한다고 해서 모델이 더 많이 쓰는 것도 아닙니다: 그것은 한도일 뿐이며, 실제로 생성된 토큰에 대해서만 비용을 지불합니다(자세한 내용은 출력 토큰 줄이기 참고).

빠른 확인

스크립트가 finish_reason = length를 출력했습니다. 어떻게 해야 할까요?

정답 보기

정답: 저장하지 않는다; max_tokens를 올리거나 요청을 줄인다

length는 답변이 한도에서 잘렸다는 뜻이므로, 스크립트의 끝부분이 빠져 있습니다. 한도를 낮추면 잘리는 지점이 더 빨리 옵니다.

열어서 페이지 확인하기#

tool.html을 더블클릭하거나 터미널에서 엽니다: macOS에서는 open tool.html, Linux에서는 xdg-open tool.html, Windows에서는 start tool.html.

이것은 모델이 당신을 위해 작성한 코드이므로, 신뢰하기 전에 잠시 시간을 들이세요. 브라우저 개발자 도구(F12)를 열고 Network 탭을 보면서 새로고침하세요: "no external requests" 제약이 있으면 비어 있어야 합니다. 그런 다음 프롬프트의 "Controls" 부분에 있는 모든 컨트롤을 한 번씩 클릭하세요.

문제가 있으면 처음부터 다시 생성하지 마세요. 특정 사항에 대한 문장 하나를 프롬프트에 추가하고("the swap button must also swap the two values") 다시 실행하세요. DeepSeek의 자체 페이지에 따르면 데모 중 일부는 여러 대화 라운드에 걸쳐 다듬어졌으므로, 두 번째 시도는 정상입니다.

직접 해보기시도해 보세요: 변환기의 Network 탭에서 무엇이 보일 것으로 예상하나요?정답 보기

아무것도 없습니다: 페이지는 인라인 CSS와 JavaScript가 있는 단일 파일이므로, 첫 로드 후에는 추가 요청이 없습니다. CDN에 대한 요청이 보이면 모델이 제약을 무시한 것이니, 프롬프트에 대문자로 추가하고 다시 실행하세요.

한 번 생성하는 데 드는 비용#

프롬프트가 약 600 입력 토큰이고 완성된 페이지가 약 6,000 출력 토큰이라고 해 봅시다(중간 규모 도구에 대한 가정이며, 실제 수치는 자신의 usage 줄에 표시됩니다). DeepSeek-V4-Flash에서 한 번 실행하면 $0.00061이고, GLM-5.3-Flash에서는 $0.00025입니다. 둘 다 현재 가격표의 실시간 수치이며, 입력과 출력을 합쳐 총액 기준으로 둘 다 OpenRouter보다 저렴합니다.

모델 비용 OpenRouter
GLM-5.3-Flash $0.00025 $0.00052
DeepSeek-V4-Flash $0.00061 $0.00088
MiniMax-M2.7 $0.00093 $0.0112

총 입력 600개 및 출력 6,000개 토큰입니다. 100만 토큰당 가격은 현재 기준이며 당사 요금제에 따라 함께 변경됩니다. OpenRouter 열은 동일한 작업량에 대한 참고 가격을 보여줍니다.

실제로는 첫 시도에 도구를 제대로 만들기 어려우므로 반복을 염두에 두세요: 프롬프트를 열 번 다듬으면 그 줄의 열 배가 되지만, 직접 작성했을 도구에 비하면 여전히 작은 금액입니다. 거의 대부분이 출력 측, 즉 페이지 자체이므로, 요청을 줄이면(컨트롤을 줄이면) 답변과 청구서가 짧아집니다.

동작하지 않을 때#

보이는 것 가능한 원인 할 일
API에서 401 또는 404 잘못된 키, base URL 또는 모델 id 세 가지를 확인하세요, API 401 또는 404 오류 참고
Cut off at max_tokens 페이지가 한도보다 김 max_tokens을 올리거나 더 적게 요청
The reply has no closing </html> 모델이 일찍 멈췄거나 텍스트를 추가함 다시 실행하세요; 프롬프트에 "end with </html>" 추가
페이지는 열리지만 버튼이 아무 동작도 안 함 JavaScript 오류 콘솔(F12)을 열고 빨간 오류를 후속 프롬프트에 붙여넣기

모델의 응답을 페이지가 아니라 데이터로 파싱하고 싶다면, 추출 요령은 LLM 응답에서 json.JSONDecodeError와 같습니다. API가 처음인가요? 첫 API 요청부터 시작하세요.