> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duruai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 두루AI API Document

> Duru AI OPEN API 연동 및 사용 가이드

> Duru AI OPEN API 연동 및 사용 가이드

<Info>
  Duru AI는 OpenAI SDK / Cursor 등 기존 OpenAI 호환 클라이언트에서 `baseURL` 과 `apiKey` 만 교체하면 그대로 사용할 수 있는 OPEN API를 제공합니다.
</Info>

<Note>
  Duru AI는 OpenAI SDK / Cursor 등 기존 OpenAI 호환 클라이언트에서 `baseURL` 과 `apiKey` 만 교체하면 그대로 사용할 수 있는 OPEN API를 제공합니다.
</Note>

## 1. 개요

* OpenAI **Chat Completions** 규격 호환
* OpenAI **Responses** 규격 호환 (Responses 전용 모델 지원)
* 사용량은 **크레딧(원화)** 으로 차감됩니다.

## 2. 기본 정보 (Base URL)

| 환경 | Base URL                        |
| -- | ------------------------------- |
| 운영 | `https://www.duruai.com/llm/v1` |

| 환경 | Base URL                        |
| -- | ------------------------------- |
| 운영 | `https://api.duruai.com/llm/v1` |

<Warning>
  OpenAI SDK의 `base_url` 에는 끝의 `/llm/v1` 까지 포함해서 입력해야 합니다.
</Warning>

## 3. 인증

모든 요청은 발급받은 API 키를 `Authorization: Bearer` 헤더에 담아 보냅니다.

```bash theme={null}
Authorization: Bearer duruai_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

* API 키는 `duruai_` 접두어로 시작합니다.
* 키는 발급 시점에 **한 번만** 전체 값이 노출되며, 서버에는 해시만 저장됩니다. 분실 시 재발급이 필요합니다.
* 개인용 키 / 조직(팀) 공유 키를 발급할 수 있습니다.

### API 키 관리 엔드포인트 (로그인 세션 필요)

| 메서드    | 경로                              | 설명                                         |
| ------ | ------------------------------- | ------------------------------------------ |
| GET    | `/llm/apis?isOrganization=true` | 키 목록 조회 (조직/개인)                            |
| POST   | `/llm/apis`                     | 키 발급 (name, description?, isOrganization?) |
| PATCH  | `/llm/apis/:uuid/status`        | 키 활성/비활성 (status: 1 또는 0)                  |
| DELETE | `/llm/apis/:uuid`               | 키 삭제                                       |

## 4. 지원 모델

`GET /llm/v1/models` 로 목록을 조회할 수 있습니다. 실제 텍스트 생성 호출이 가능한 모델은 다음과 같습니다.

| 모델 (model 값)            | 제공사        |
| ----------------------- | ---------- |
| `gpt-5.4-mini`          | OpenAI     |
| `gpt-5.4`               | OpenAI     |
| `gpt-5.4-pro`           | OpenAI     |
| `gpt-5.5`               | OpenAI     |
| `gemini-3-flash`        | Google     |
| `gemini-3.1-flash-lite` | Google     |
| `gemini-3.1-pro`        | Google     |
| `gemini-3.5-flash`      | Google     |
| `grok-4-3`              | xAI        |
| `grok-4-5`              | xAI        |
| `claude-4.5-haiku`      | Claude     |
| `claude-4.6-sonnet`     | Claude     |
| `claude-4.6-opus`       | Claude     |
| `claude-4.7-opus`       | Claude     |
| `claude-4.8-opus`       | Claude     |
| `claude-5-sonnet`       | Claude     |
| `sonar`                 | Perplexity |
| `sonar-pro`             | Perplexity |
| `sonar-reasoning-pro`   | Perplexity |
| `sonar-deep-research`   | Perplexity |
| `solar-pro3`            | Upstage    |
| `solar-pro2`            | Upstage    |
| `solar-mini`            | Upstage    |

<Check>
  claude-4.7-opus / claude-4.8-opus 는 `temperature` 파라미터가 자동 무시됩니다. gpt-5.4-pro / gpt-5.5 는 `temperature` 와 `top_p` 파라미터가 모두 자동 무시됩니다.
</Check>

## 5. 엔드포인트

### 5-1. 모델 목록 조회

```javascript theme={null}
GET /llm/v1/models
```

OpenAI Models API와 동일한 `{ "object": "list", "data": [...] }` 형식으로 반환됩니다.

### 5-2. Chat Completions

```javascript theme={null}
POST /llm/v1/chat/completions
```

**요청 본문**

| 필드            | 타입      | 필수 여부    | 설명                                                                                                  |
| ------------- | ------- | -------- | --------------------------------------------------------------------------------------------------- |
| `model`       | string  | Required | 모델 slug                                                                                             |
| `messages`    | array   | Required | role/content 배열 (system/user/assistant)                                                             |
| `stream`      | boolean | Optional | true 시 SSE 스트리밍 (기본값: false)                                                                        |
| `temperature` | number  | Optional | 범위 0.0\~2.0, 기본값 1.0. `claude-4.7-opus` / `claude-4.8-opus` / `gpt-5.4-pro` / `gpt-5.5` 호출 시 자동 무시됨 |
| `top_p`       | number  | Optional | 범위 0.0\~1.0, 기본값 1.0. `gpt-5.4-pro` / `gpt-5.5` 호출 시 자동 무시됨                                         |
| `max_tokens`  | number  | Optional | 최대 출력 토큰                                                                                            |

**성공 응답 예시 (200 OK)**

```json theme={null}
{
  "id": "chatcmpl-1749100000000",
  "object": "chat.completion",
  "created": 1749100000,
  "model": "gpt-5.4-mini",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "안녕하세요! 무엇을 도와드릴까요?"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 12,
    "total_tokens": 37
  }
}
```

### 5-3. Responses

```javascript theme={null}
POST /llm/v1/responses
```

Responses 전용 모델까지 Cursor / OpenAI SDK에서 쓸 수 있도록 OpenAI Responses 규격을 호환합니다.

**요청 본문**

| 필드                  | 타입           | 필수 여부    | 설명                                                                                             |
| ------------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------- |
| `model`             | string       | Required | 모델 slug                                                                                        |
| `input`             | string/array | Required | 입력 (문자열 또는 role/content 배열)                                                                    |
| `instructions`      | string       | Optional | 시스템 지시문 (system 메시지로 변환)                                                                       |
| `stream`            | boolean      | Optional | true 시 Responses SSE 이벤트 스트리밍 (기본값: false)                                                     |
| `temperature`       | number       | Optional | 범위 0.0\~2.0, 기본값 1.0 (`claude-4.7-opus` / `claude-4.8-opus` / `gpt-5.4-pro` / `gpt-5.5` 자동 무시) |
| `top_p`             | number       | Optional | 범위 0.0\~1.0, 기본값 1.0 (`gpt-5.4-pro` / `gpt-5.5` 자동 무시)                                         |
| `max_output_tokens` | number       | Optional | 최대 출력 토큰                                                                                       |

**성공 응답 예시 (200 OK)**

```json theme={null}
{
  "id": "resp-1749100000000",
  "object": "response",
  "created_at": 1749100000,
  "model": "gpt-5.4",
  "output_text": "안녕하세요! 무엇을 도와드릴까요?",
  "output": [
    {
      "id": "resp-1749100000000-msg",
      "type": "message",
      "status": "completed",
      "role": "assistant",
      "content": [
        {
          "type": "output_text",
          "text": "안녕하세요! 무엇을 도와드릴까요?",
          "annotations": []
        }
      ]
    }
  ],
  "usage": {
    "input_tokens": 10,
    "input_tokens_details": { "cached_tokens": 0 },
    "output_tokens": 12,
    "output_tokens_details": { "reasoning_tokens": 0 },
    "total_tokens": 22
  }
}
```

## 6. 사용 예시

```bash cURL theme={null}
curl https://www.duruai.com/llm/v1/chat/completions \
  -H "Authorization: Bearer $DURUAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.4-mini",
    "messages": [
      { "role": "system", "content": "너는 친절한 한국어 비서야." },
      { "role": "user", "content": "안녕!" }
    ]
  }'
```

```python Python theme={null}
from openai import OpenAI

client = OpenAI(
    api_key="duruai_xxxxxxxxxxxxxxxxxxxx",
    base_url="https://www.duruai.com/llm/v1",
)

# Chat Completions
res = client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=[{"role": "user", "content": "안녕!"}],
)
print(res.choices[0].message.content)

# Responses
res = client.responses.create(
    model="gpt-5.4",
    input="한 문장으로 자기소개 해줘",
)
print(res.output_text)
```

```javascript Node.js theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.DURUAI_API_KEY,
  baseURL: "https://www.duruai.com/llm/v1",
});

// Chat Completions
const res = await client.chat.completions.create({
  model: "gemini-3-flash",
  messages: [{ role: "user", content: "안녕!" }],
});
console.log(res.choices[0].message.content);
```

### 스트리밍 (Streaming)

`stream: true` 로 설정하면 OpenAI와 동일하게 `data: ...` (Chat) / `event: response.* ...` (Responses) SSE 청크가 전송되며, 마지막에 `data: [DONE]` 으로 종료됩니다.

#### cURL (Chat Completions - 스트리밍)

```bash cURL theme={null}
curl https://www.duruai.com/llm/v1/chat/completions \
  -H "Authorization: Bearer $DURUAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.4-mini",
    "stream": true,
    "messages": [
      { "role": "system", "content": "너는 친절한 한국어 비서야." },
      { "role": "user", "content": "안녕!" }
    ]
  }'
```

#### Python (OpenAI SDK - 스트리밍)

```python Python theme={null}
from openai import OpenAI

client = OpenAI(
    api_key="duruai_xxxxxxxxxxxxxxxxxxxx",
    base_url="https://www.duruai.com/llm/v1",
)

# Chat Completions 스트리밍
with client.chat.completions.stream(
    model="gpt-5.4-mini",
    messages=[{"role": "user", "content": "안녕!"}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

# Responses 스트리밍
with client.responses.stream(
    model="gpt-5.4",
    input="한 문장으로 자기소개 해줘",
) as stream:
    for event in stream:
        if event.type == "response.output_text.delta":
            print(event.delta, end="", flush=True)
```

#### Node.js (OpenAI SDK - 스트리밍)

```javascript Node.js theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.DURUAI_API_KEY,
  baseURL: "https://www.duruai.com/llm/v1",
});

// Chat Completions 스트리밍
const stream = await client.chat.completions.create({
  model: "gpt-5.4-mini",
  stream: true,
  messages: [{ role: "user", content: "안녕!" }],
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

// Responses 스트리밍
const responseStream = await client.responses.create({
  model: "gpt-5.4",
  stream: true,
  input: "한 문장으로 자기소개 해줘",
});

for await (const event of responseStream) {
  if (event.type === "response.output_text.delta") {
    process.stdout.write(event.delta);
  }
}
```

### SSE 응답 데이터 형식

#### Chat Completions 스트리밍 응답

각 청크는 `data: {...}` 형식으로 전송됩니다.

```javascript theme={null}
# 텍스트 청크 (여러 번 전송됨)
data: {"id":"chatcmpl-1749100000000","object":"chat.completion.chunk","created":1749100000,"model":"gpt-5.4-mini","choices":[{"index":0,"delta":{"content":"안녕"},"finish_reason":null}]}

# 마지막 청크 (usage 포함)
data: {"id":"chatcmpl-1749100000000","object":"chat.completion.chunk","created":1749100000,"model":"gpt-5.4-mini","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":25,"completion_tokens":12,"total_tokens":37}}

# 종료 시그널
data: [DONE]
```

#### Responses 스트리밍 응답

`event: 이벤트명` + `data: {...}` 쌍으로 전송됩니다.

```javascript theme={null}
event: response.created
data: {"type":"response.created","response":{"id":"resp-1749100000000","object":"response","created_at":1749100000,"model":"gpt-5.4","output_text":"","output":[{"id":"resp-1749100000000-msg","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"","annotations":[]}]}],"usage":{"input_tokens":0,"input_tokens_details":{"cached_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":0}}}

event: response.in_progress
data: {"type":"response.in_progress","response_id":"resp-1749100000000"}

event: response.output_item.added
data: {"type":"response.output_item.added","response_id":"resp-1749100000000","output_index":0,"item":{"id":"resp-1749100000000-msg","type":"message","status":"in_progress","role":"assistant","content":[]}}

event: response.content_part.added
data: {"type":"response.content_part.added","response_id":"resp-1749100000000","item_id":"resp-1749100000000-msg","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}

# 텍스트 델타 (여러 번 전송됨)
event: response.output_text.delta
data: {"type":"response.output_text.delta","response_id":"resp-1749100000000","item_id":"resp-1749100000000-msg","output_index":0,"content_index":0,"delta":"안녕하세요!"}

event: response.output_text.done
data: {"type":"response.output_text.done","response_id":"resp-1749100000000","item_id":"resp-1749100000000-msg","output_index":0,"content_index":0,"text":"안녕하세요! 무엇을 도와드릴까요?"}

event: response.content_part.done
data: {"type":"response.content_part.done","response_id":"resp-1749100000000","item_id":"resp-1749100000000-msg","output_index":0,"content_index":0,"part":{"type":"output_text","text":"안녕하세요! 무엇을 도와드릴까요?","annotations":[]}}

event: response.output_item.done
data: {"type":"response.output_item.done","response_id":"resp-1749100000000","output_index":0,"item":{"id":"resp-1749100000000-msg","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"안녕하세요! 무엇을 도와드릴까요?","annotations":[]}]}}

event: response.completed
data: {"type":"response.completed","response":{"id":"resp-1749100000000","object":"response","created_at":1749100000,"model":"gpt-5.4","output_text":"안녕하세요! 무엇을 도와드릴까요?","output":[{"id":"resp-1749100000000-msg","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"안녕하세요! 무엇을 도와드릴까요?","annotations":[]}]}],"usage":{"input_tokens":10,"input_tokens_details":{"cached_tokens":0},"output_tokens":12,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":22}}}

# 종료 시그널
data: [DONE]
```

## Cursor 연동

Cursor의 OpenAI 호환 설정에서 Override Base URL 에 `https://www.duruai.com/llm/v1`, API Key 에 발급받은 `duruai_...` 키를 입력하면 됩니다.

## 7. 과금

* 요청 성공 시 `입력/출력 토큰 × 모델 단가(USD) × 환율 → 원화` 로 환산해 크레딧을 차감합니다.
* 환율은 **한국수출입은행 API 기준 전일 최종 환율**을 사용합니다 (매일 자동 갱신, 고정 환율 아님).
* 모델/조직별 **할인율**이 적용될 수 있습니다.
* 요청 직전 잔액이 **1원 미만이면 호출이 차단**됩니다. (조직 키는 조직 잔액 기준)

## 8. 응답 상태 코드

오류 발생 시 HTTP 4xx 상태 코드와 함께, 아래 형식의 JSON이 반환됩니다. `status` 필드는 HTTP 상태 코드가 아닌 **앱 전용 에러 코드**입니다.

| status (앱 코드) | HTTP 상태 코드 | 의미                |
| ------------- | ---------- | ----------------- |
| 100           | 400        | 요청 값 검증 오류        |
| 110           | 401        | 권한 오류 / 잘못된 API 키 |
| 120           | 404        | 대상 없음             |
| 300           | 400 / 500  | LLM 처리 오류         |
| 400           | 400        | 조직 크레딧 부족         |
| 401           | 400        | 사용자 크레딧 부족        |

**오류 응답 예시**

```json theme={null}
{
  "status": 110,
  "message": "Invalid API key"
}
```

```bash cURL theme={null}
curl https://api.duruai.com/llm/v1/chat/completions \
  -H "Authorization: Bearer $DURUAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.4-mini",
    "messages": [
      { "role": "system", "content": "너는 친절한 한국어 비서야." },
      { "role": "user", "content": "안녕!" }
    ]
  }'
```

```python Python theme={null}
from openai import OpenAI

client = OpenAI(
    api_key="duruai_xxxxxxxxxxxxxxxxxxxx",
    base_url="https://api.duruai.com/llm/v1",
)

# Chat Completions
res = client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=[{"role": "user", "content": "안녕!"}],
)
print(res.choices[0].message.content)

# Responses
res = client.responses.create(
    model="gpt-5.4",
    input="한 문장으로 자기소개 해줘",
)
print(res.output_text)
```

```javascript Node.js theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.DURUAI_API_KEY,
  baseURL: "https://api.duruai.com/llm/v1",
});

// Chat Completions
const res = await client.chat.completions.create({
  model: "gemini-3-flash",
  messages: [{ role: "user", content: "안녕!" }],
});
console.log(res.choices[0].message.content);
```

```bash cURL theme={null}
curl https://api.duruai.com/llm/v1/chat/completions \
  -H "Authorization: Bearer $DURUAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.4-mini",
    "stream": true,
    "messages": [
      { "role": "system", "content": "너는 친절한 한국어 비서야." },
      { "role": "user", "content": "안녕!" }
    ]
  }'
```

```python Python theme={null}
from openai import OpenAI

client = OpenAI(
    api_key="duruai_xxxxxxxxxxxxxxxxxxxx",
    base_url="https://api.duruai.com/llm/v1",
)

# Chat Completions 스트리밍
with client.chat.completions.stream(
    model="gpt-5.4-mini",
    messages=[{"role": "user", "content": "안녕!"}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

# Responses 스트리밍
with client.responses.stream(
    model="gpt-5.4",
    input="한 문장으로 자기소개 해줘",
) as stream:
    for event in stream:
        if event.type == "response.output_text.delta":
            print(event.delta, end="", flush=True)
```

```javascript Node.js theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.DURUAI_API_KEY,
  baseURL: "https://api.duruai.com/llm/v1",
});

// Chat Completions 스트리밍
const stream = await client.chat.completions.create({
  model: "gpt-5.4-mini",
  stream: true,
  messages: [{ role: "user", content: "안녕!" }],
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

// Responses 스트리밍
const responseStream = await client.responses.create({
  model: "gpt-5.4",
  stream: true,
  input: "한 문장으로 자기소개 해줘",
});

for await (const event of responseStream) {
  if (event.type === "response.output_text.delta") {
    process.stdout.write(event.delta);
  }
}
```

Cursor의 OpenAI 호환 설정에서 Override Base URL 에 `https://api.duruai.com/llm/v1`, API Key 에 발급받은 `duruai_...` 키를 입력하면 됩니다.
