본문으로 건너뛰기
runlot
데이터AI

AI

worker에서 언어 모델을 호출합니다. env.ai 바인딩 하나로 frontier 모델과 value 모델 모두에 접근할 수 있으며, 비용은 조직의 크레딧 잔액에서 차감됩니다.

첫 배포부터 워커에서 env.ai를 사용할 수 있습니다. 따로 켤 것은 없습니다.

env.ai는 worker 내부에서만 존재합니다. worker 진입점 없이 정적 자산만 배포하는 프로젝트는 사용할 수 없습니다.

호출하기

const r = await env.ai.run("anthropic/claude-sonnet-5", {
  messages: [{ role: "user", content: "Write the order confirmation email." }],
  maxTokens: 1024,
});

r.text        // the generated text
r.usage       // { inputTokens, outputTokens, cachedInputTokens }
r.model          // the model that actually ran
r.stopReason     // why generation stopped
r.usageComplete  // whether usage is the exact final total

호출이 성공했어도 text가 비어 있을 수 있습니다. 특히 추론 모델은 보이는 텍스트를 내보내기 전에 출력 예산을 추론에 모두 쓰고 stopReason: "max_tokens"로 멈출 수 있습니다. stopReason, usage, usageComplete를 함께 기록하세요. 빈 문자열만으로는 모델이 답할 내용이 없었는지, 예산을 다 썼는지 알 수 없습니다.

usageComplete가 false이면 usage는 종료 전에 Runlot이 관측한 프로바이더 보고 소계일 뿐입니다. 0이라고 해서 프로바이더가 작업하지 않았거나 비용이 발생하지 않았다는 뜻은 아닙니다. Runlot은 누락된 토큰을 만들어 내지 않습니다.

메시지가 하나뿐이라면 prompt가 축약형입니다. 사용자 메시지 하나와 동일합니다.

const r = await env.ai.run("@runlot/nova-micro", { prompt: "Summarise this in one line." });
옵션
messages{ role, content } 배열
prompt단일 사용자 메시지의 축약형
system시스템 프롬프트
maxTokens생성할 최대 토큰 수
temperature샘플링 온도
reasoning{ effort } — 모델이 답하기 전에 얼마나 깊이 생각하는지 지정합니다. 모델별 값은 env.ai.models() 행의 effortLevels에 있습니다. 생략하면 프로바이더 기본값으로 실행됩니다
responseFormat{ type: "json_schema", name?, schema } — 답변이 schema를 따르는 JSON이 됩니다. env.ai.models() 행에 structuredOutput: true가 있는 모델에서만 동작합니다
tools서버측 도구: { name, description, inputJsonSchema, execute }[]
maxToolRounds모델/도구 라운드 상한, 1–32(기본값 8)
streamtrue이면 ReadableStream을 반환합니다

role은 user 또는 assistant만 가능합니다. 시스템 프롬프트는 messages가 아니라 system에 넣습니다.

Next.js 프로젝트에서는 @runlot/next에서 env를 import하세요. 이 패키지가 내보내는 RunlotEnv 타입에 env.ai가 포함되므로 별도의 ambient 선언 없이 run, embed, models, 도구 결과, 사용량, 종료 결과에 타입이 적용됩니다.

Studio와 runlot dev 중계는 stopReason, cachedInputTokens, usageComplete를 보존합니다. 로컬 코드에서도 중계 경계에서 종료 메타데이터를 잃지 않고 같은 값을 받습니다.

추론 노력과 구조화 출력

reasoning.effort는 답변의 깊이와 속도·비용을 맞바꿉니다. 값은 모델 계열마다 다르므로 env.ai.models()가 각 모델의 값을 effortLevels로 알려줍니다 — OpenAI 행은 none부터 max, Claude 행은 low부터 max, Gemini는 low/medium/high입니다. 모델이 갖지 않은 값을 요구하면 400 bad_effort가 허용 값 목록과 함께 오고, 노력 조절이 아예 없는 모델(@runlot/…)은 옵션을 무시하지 않고 거부합니다.

responseFormat은 답변을 여러분의 JSON Schema로 제약하므로, 도구 루프 없이 JSON.parse(r.text)가 바로 설명한 객체가 됩니다.

const r = await env.ai.run("openai/gpt-6-luna", {
  prompt: "주문 A-123은 화요일에 발송되었습니다. 추출해 주세요.",
  reasoning: { effort: "low" },
  responseFormat: {
    type: "json_schema",
    schema: {
      type: "object",
      properties: { orderId: { type: "string" }, status: { type: "string" } },
      required: ["orderId", "status"],
      additionalProperties: false,
    },
  },
});
const order = JSON.parse(r.text); // { orderId, status }

어떤 모델이 지원하는지는 env.ai.models() 행(structuredOutput: true)에 있습니다 — 지금은 openai/* 전부와 anthropic/claude-haiku-4-5, 두 Gemini 행입니다. 나머지는 산문을 조용히 반환하지 않고 400 format_unsupported로 답합니다. 두 조합은 이름으로 거부됩니다: responseFormat과 stream의 동시 사용(프로바이더가 비스트리밍 호출로 범위를 한정합니다), 그리고 tools와의 동시 사용(도구 인자를 모양짓는 방법은 도구 자신의 inputJsonSchema입니다). 스키마는 프로바이더의 제약 디코더가 받는 형태로 쓰세요: 객체 스키마, 모든 필드가 required, additionalProperties: false.

서버측 도구

execute는 env.ai.run을 호출한 바로 그 앱 worker 세대 안에서 실행됩니다. 모델에는 도구 이름, 설명, JSON Schema만 전달되며 callback URL, 자격 증명, 함수 코드는 worker 밖으로 나가지 않습니다.

const answer = await env.ai.run("anthropic/claude-sonnet-5", {
  prompt: "주문 A-123은 어디에 있나요?",
  tools: [{
    name: "lookup_order",
    description: "현재 사용자의 주문을 조회합니다.",
    inputJsonSchema: {
      type: "object",
      properties: { orderId: { type: "string" } },
      required: ["orderId"],
      additionalProperties: false,
    },
    async execute({ orderId }, { signal }) {
      const order = await lookupOrder(orderId, { signal });
      if (order.status === "cancelled") {
        return { stop: true, text: "주문이 취소되었습니다.", data: order };
      }
      return { data: order };
    },
  }],
});

{ text }, { data }, { isError: true, text }, 또는 { stop: true, text?, data? }를 반환합니다. 예외를 던지면 모델이 처리할 수 있는 오류 결과가 되며 호출 전체가 자동으로 끝나지는 않습니다. stop: true는 isError가 true가 아닌 경우에만 종료 결과입니다. Runlot은 그 결과를 제출하고 식별된 원격 실행을 취소한 뒤 취소 확인을 기다립니다. 이후의 도구 호출은 실행하지 않고 새 모델 라운드도 시작하지 않은 채 즉시 결과를 반환합니다.

종료된 비스트리밍 결과에는 멈추기 전까지 관측한 모든 값이 남습니다.

{
  text,                    // 도구가 멈추기 전까지 누적된 모델 텍스트
  model,
  stopReason: "tool_stop",
  usage,                   // 관측된 누적 사용량
  usageComplete,
  finalTool: { id, name, args, result },
}

크레딧으로 실행된 tool_stop에서는 usageComplete가 true입니다. 이 경로는 도구 호출을 노출하기 전에 완료된 프로바이더 라운드마다 사용량을 보고하고 정산합니다. Studio를 통해 Claude 또는 Codex 구독으로 실행한 호출이 대기 중인 도구에서 멈추면 false입니다. 이 어댑터들은 실행이 완료될 때만 사용량을 보고하므로 Runlot에는 앞서 완료된 작업의 소계만 있습니다. 정상적으로 완료된 실행은 true, 모든 오류는 false입니다. BYOS 감사 기록도 최종 합계를 지어내지 않고 같은 관측 소계를 기록합니다.

취소되었다고 해서 프로바이더 작업에 비용이 전혀 발생하지 않았다고 보장할 수는 없습니다. 서비스가 실행 ID를 보내기 전에는 wrapper가 확인할 원격 실행이 없으며, 중단 시 프로바이더가 Runlot에 보고하지 않은 사용량이 있을 수 있습니다. 현재 도구는 anthropic/*와 openai/*에서 지원합니다. 다른 모델 계열은 도구를 조용히 버리지 않고 tools_unsupported를 반환합니다.

스트리밍

stream: true를 사용하면 SSE 프레임의 ReadableStream을 받습니다. 이를 그대로 Response에 전달하세요. 도구 호출 중에는 생성이 잠시 멈추고 execute가 끝난 뒤의 텍스트가 같은 스트림에서 이어집니다. 도구 제어 프레임은 텍스트로 노출되지 않습니다.

const stream = await env.ai.run("@runlot/nova-micro", { prompt, stream: true });
return new Response(stream, { headers: { "content-type": "text/event-stream" } });

프레임은 다음과 같은 형태입니다. 도구가 있는 공개 스트림은 usage 이벤트도 전달합니다. 크레딧 경로에서 cumulative: true가 없는 이벤트는 한 라운드의 증가분이므로 실행 중인 합계에 더합니다. 구독 경로에서 cumulative: true인 이벤트는 지금까지 관측한 전체를 담은 교체 스냅샷입니다. 더하지 말고 실행 중인 합계를 교체해야 마지막 스냅샷을 두 번 세지 않습니다. 마지막 done 또는 error 프레임에는 마지막 라운드만이 아니라 누적 text와 usage가 들어 있습니다.

data: {"delta":"Hello"}

event: usage
data: {"inputTokens":12,"outputTokens":3,"cachedInputTokens":4,"round":1}

data: {"delta":" there"}

event: done
data: {"text":"Hello there","model":"anthropic/claude-sonnet-5","stopReason":"end_turn","usage":{"inputTokens":12,"outputTokens":3,"cachedInputTokens":4},"usageComplete":true}

생성이 중간에 중단되면 마지막 프레임은 event: error입니다.

event: error
data: {"code":"upstream_error","message":"…","text":"Hello","model":"anthropic/claude-sonnet-5","usage":{"inputTokens":12,"outputTokens":3,"cachedInputTokens":4},"usageComplete":false}

마지막 프레임은 누적 사용량이 완전한지 항상 알려줍니다. usageComplete가 false이면 중단 전에 Runlot이 관측한 프로바이더 보고 소계만 들어 있으며, 0도 관측 소계일 뿐입니다. 프로바이더가 소비했지만 보고하지 않은 토큰까지 보장할 수는 없습니다. 스트림이 시작된 후에는 오류가 예외로 발생하지 않고 이 프레임으로 전달됩니다. 200 응답은 이미 첫 프레임과 함께 전송되었기 때문입니다. 스트림을 읽는 모든 곳에서 event: error를 처리하세요.

카탈로그

이 프로젝트의 코드에서 실제로 호출할 수 있는 모델 목록을 확인합니다.

const models = await env.ai.models();

두 가지 등급이 있으며, 가격 책정 방식이 다릅니다.

등급모델 ID가격
Frontieranthropic/… · openai/… · google/…provider의 정가를 마진 없이 그대로 적용
값@runlot/…runlot이 설정

카탈로그에 없는 모델 ID는 400을 반환합니다. 목록은 저희가 직접 관리하는 고정된 목록입니다.

각 행에는 kind가 있습니다. env.ai.run 용은 chat, env.ai.embed 용은 embedding 입니다. 임베딩과 그에 딸린 벡터 저장소는 별도 페이지에 있습니다.

무료 할당량

모든 조직은 value 모델에 사용할 수 있는 하루 $0.11을 받습니다. UTC 자정에 초기화됩니다. 크레딧이 전혀 없어도 매일 이 한도까지는 @runlot/… 모델을 호출할 수 있습니다.

Frontier 모델에는 무료 할당량이 없습니다. 일일 한도를 frontier 모델에도 열어두면 호출 한 번으로 전부 소진될 수 있습니다. 이는 무료 등급이 아니라 결함입니다. anthropic/…, openai/…, google/…는 조직에 크레딧이 있어야 하며, 없으면 402가 반환됩니다.

크레딧은 돈입니다

크레딧은 토큰 묶음이 아니라 금액 잔액입니다. 호출이 차감하는 금액은 호출 시점의 가격표를 따릅니다.

provider의 정가는 변동됩니다. 예를 들어 Google의 가격은 2027-01-01에 두 배가 됩니다. 크레딧이 토큰 묶음이었다면 가격 변경으로 사용자가 이미 구매한 크레딧의 가치가 알게 모르게 달라졌을 것입니다. 금액 잔액 방식은 이를 막습니다. 구매한 것은 금액이고, 그것으로 무엇을 살 수 있는지는 호출 시점에 결정됩니다.

두 등급 모두 같은 통화로 가격이 책정됩니다. 유일한 차이는 가격의 출처뿐이며, 따로 익혀야 할 단위는 없습니다.

화면과 CLI는 USD로 표시합니다. API와 --json 출력의 정수 값은 마이크로센트입니다(1 마이크로센트 = 1e-8 USD). 센트 단위였다면 작은 호출은 반올림되어 0이 되고 알게 모르게 무료가 되었을 것입니다. 토큰 천 개짜리 호출도 1센트의 백분의 일 미만이기 때문입니다.

usage.inputTokens는 전체 입력입니다. usage.cachedInputTokens는 그 전체에 포함된 캐시 입력 부분이며, 따로 더하는 수가 아닙니다. Runlot은 프로바이더마다 다른 캐시 카운터를 이 형태로 정규화한 뒤 가격을 계산합니다. 캐시 읽기에는 캐시 입력 요율을, 캐시되지 않은 입력과 캐시 쓰기에는 일반 입력 요율을 적용합니다. 이 정규화의 수정은 수정된 릴리스 이후 호출에만 적용되며, 이전 원장 항목에 소급 청구를 만들지 않습니다.

제한 사항

무료프로
동시 호출 수(프로젝트당)210
분당 호출 수(프로젝트당)30300
호출당 maxTokens4,096모델 최대값
호출당 입력128k 토큰모델 최대값
무료 일일 허용량(가치)org당 $0.11동일

오류

거부되거나 실패한 호출은 예외를 던집니다. 스트림이 시작되기 전에 발생한 오류에는 e.status(HTTP 상태)와 e.code(짧은 문자열)가 담깁니다. 도구가 있는 실행이 시작된 뒤 실패하면 오류에 e.text, e.model, 누적 e.usage, e.usageComplete: false도 들어가므로 복구 코드가 실패 전까지 관측한 텍스트와 프로바이더 보고 소계를 보존할 수 있습니다. 이 소계는 관측값이며, 보이지 않은 프로바이더 비용이 0이라는 보장은 아닙니다.

callback이 이미 { stop: true }를 반환했지만 취소 확인이 실패한 경우 오류에 e.finalTool도 들어갈 수 있습니다. callback의 부수 효과는 이미 반영되었을 수 있습니다. env.ai.run이 예외를 던졌다는 이유만으로 도구를 다시 실행하지 마세요. 먼저 finalTool.id, 인자, 앱의 멱등성 기록을 확인하세요. 스트림 오류도 같은 finalTool 필드를 사용합니다.

상태code뜻
400unknown_model모델 id가 카탈로그에 없습니다. env.ai.models()가 그 목록입니다.
400bad_effortreasoning.effort가 그 모델의 값이 아닙니다. 메시지가 허용 값을 나열합니다.
400bad_formatresponseFormat의 형태가 잘못됐습니다 — type이 json_schema가 아니거나, schema가 없거나, name이 잘못됐습니다.
400format_unsupported그 모델에 구조화 출력이 없거나, responseFormat을 stream 또는 tools와 함께 요구했습니다.
402no_credit크레딧이 없거나 무료 일일 허용량을 다 썼습니다.
413input_too_large입력이 호출당 상한을 초과했습니다.
429rate_limited분당 제한입니다.
429too_many_concurrent동시성 제한입니다.
429upstream_rate_limited프로바이더가 바쁩니다.
502upstream_error프로바이더 호출이 실패했습니다.
503model_unavailable이 배포에서는 해당 모델을 호출할 수 없습니다.
504upstream_timeout프로바이더 호출이 시간 초과되었습니다.

rate_limited와 upstream_rate_limited에는 Retry-After 응답 헤더가 함께 옵니다. too_many_concurrent에는 없습니다 — 언제 자리가 비는지 알 수 없기 때문입니다. 진행 중인 호출이 끝나면 해소됩니다.

충전

대시보드의 org 화면에서 Billing 탭으로 충전하세요.

자동 충전

잔액이 설정한 수준 아래로 떨어지면 저장된 카드로 자동 결제됩니다. 기본값은 꺼짐입니다.

충전 금액$10·$20·$50·$100 중 하나
임계값0보다 크고 충전 금액보다 작은 값
일일 청구 횟수5
연속 실패3회면 꺼집니다
  • 첫 충전은 수동입니다. 카드는 그 결제 시점에 저장됩니다. 저장된 결제 수단이 없으면 자동 충전을 켤 수 없습니다.
  • 켜는 것은 명시적 동의입니다. 결제 화면에 있지 않은 상태에서 카드가 청구되므로, 화면에 청구 조건을 전부 명시하고 확인을 요청합니다.
  • 하루 다섯 번 청구가 지출 상한입니다. 일일 최대치는 충전 금액의 5배입니다. 루프로 호출하는 버그성 코드는 거기서 멈춥니다. 횟수는 조정할 수 없으며 — 대신 충전 금액을 올리세요.
  • 임계값은 0보다 커야 합니다. 충전은 비동기적으로 처리되므로, 충전이 반영될 때까지 버틸 만큼 잔액이 남아 있어야 합니다. 임계값 0은 거부됩니다.
  • 임계값은 충전 금액보다 작아야 합니다. 그렇지 않으면 한 번 충전해도 여전히 임계값 아래에 머물러 일일 상한까지 계속 충전됩니다. 이 경우도 거부됩니다.
  • 연속 3회 거절되면 꺼집니다. 실패할 때마다 알림을 보내며, 세 번째 실패 시 설정이 꺼집니다. 고객의 카드를 조용히 무한정 재시도하지 않습니다.

CLI

runlot ai                 # calls, tokens, money spent, and your limits
runlot ai models          # the catalog with per-1M-token prices
runlot ai credit          # the org's credit balance and auto top-up settings
runlot ai delete          # turn it off (admin)

세 조회 명령 모두 --json을 받습니다. 모델을 호출하는 명령은 없습니다 — CLI는 워커 바깥에 있고, env.ai는 그 안에서만 접근할 수 있습니다.

여기 없는 것

  • 임의 모델 패스스루. 카탈로그는 저희가 선정하고 관리하는 고정 목록입니다.
  • 외부에서 들어오는 경로. env.ai는 배포된 워커 내부에서만 접근할 수 있습니다. API 키도, 외부 엔드포인트도, runlot 바깥에서 호출할 방법도 없습니다. 이는 잊고 빠뜨린 기능이 아니라 저희가 그은 경계입니다.
  • 직접 키 사용(BYOK). 사용자의 프로바이더 키를 받아 저희를 거쳐 호출하는 경로는 없습니다.
  • 파인튜닝, 이미지·오디오 모델. 아직 없습니다. 임베딩과 벡터 검색은 있습니다 — 임베딩과 벡터를 참고하세요.

이 페이지의 목차