Skill: upstage-deployment/upstage-builder (version 1.0.0)

--- name: upstage-builder description: "Build pipelines, agents, RAG flows, and full web services by combining Upstage Solar models, embeddings, and document APIs. Use when building, scaffolding, or deploying anything with Upstage — '솔라로 RAG 만들어줘', 'Upstage 웹앱 만들어줘', 'Upstage로 에이전트 짜줘', 'build a RAG with Solar', 'create an Upstage-based app'. Covers Solar Pro3/Pro2/Mini, embeddings, OCR, document parse, information extraction, classification, schema generation, and Agent API. For single-API one-shot calls (just OCR, just classify), prefer the dedicated upstage-<api> skill instead." --- # upstage-builder — Upstage API and Webapp Delivery Skill You are an expert at generating code that uses the **Upstage API** (`api.upstage.ai`). When the user asks you to build features or services using Upstage/Solar models, follow this guide. For full webapp requests, do not stop at code generation. Treat project location, environment variables, deployment method, and shareable URL delivery as part of the task. ## Webapp Setup Rules When the user asks for a full web service/app built with Upstage, follow this startup flow: 1. If deployment system and project root are not already known, ask once: - Which deployment system should be used? - Which project root should be used? 2. If defaults are already configured, use them. 3. For this installation, default to: - project root: `/data/.openclaw/workspace/projects` - deployment provider: `vercel` - visibility mode: `password-protected` 4. Prefer password-protected or private delivery over public delivery unless the user explicitly asks for public access. 5. Create one folder per app under the configured project root. 6. Return these at the end whenever possible: - project path - stack used - required environment variables - deployment method - visibility mode - external deployment URL, or the exact next step if deployment could not be completed - site password if password-protected mode was used Read `references/webapp-workflow.md` for the full project/deployment workflow. ## Quick Start Upstage APIs are **OpenAI SDK compatible**. Just change `base_url`: ```python from openai import OpenAI import os client = OpenAI( [REDACTED_SECRET]"UPSTAGE_API_KEY"], base_url="https://api.upstage.ai/v1" ) response = client.chat.completions.create( model="solar-pro3", messages=[{"role": "user", "content": "Hello!"}] ) print(response.choices[0].message.content) ``` **API Key**: Always use `os.environ["UPSTAGE_API_KEY"]`. Never hardcode keys. Users get their key from [console.upstage.ai](https://console.upstage.ai). --- ## Output Files When generated code writes intermediate result files (extracted JSON, parsed markdown, embeddings cache, etc.): - **Default**: `<system-temp>/<input-stem>.<suffix>.<ext>` (e.g., `/tmp/receipt.ocr.json`, `/tmp/report.parsed.md`). Use `tempfile.gettempdir()` for cross-platform code. - **Override**: if the user specifies an output path, use it. - **Always print the resolved absolute path** so the user can locate the file. This rule does NOT apply to webapp scaffolding (project root, `.env`, `DEPLOY.md`) — those follow the configured project root in **Webapp Setup Rules** above. Per-API suffix convention (matches the dedicated specialty skills): | API | Suffix | Common ext | |-----|--------|-----------| | OCR | `.ocr` | `.json` | | Document Parse | `.parsed` | `.md`, `.html` | | Document Classification | `.classified` | `.json` | | Information Extraction | `.extracted` | `.json` | | Schema Generation | `.schema` | `.json` | | Agent (Studio) | `.agent` (or `.<step-name>` per step) | `.json` | | Solar (delegated) | `.solar` (with timestamp prefix) | `.md`, `.txt` | --- ## Model Catalog ### Chat Models | Model | Description | Context | Best For | |-------|-------------|---------|----------| | `solar-pro3` | Flagship (102B MoE, 12B active) | 128K | Complex reasoning, function calling, structured output | | `solar-pro2` | Previous gen flagship (31B) | 65K | General tasks, good balance | | `solar-mini` | Lightweight, fast (10.7B) | 32K | Cost-sensitive, simple tasks | | `syn-pro` | Synthetic data optimized | - | Data generation (no function calling) | ### Embedding Models | Model | Description | Dimensions | |-------|-------------|------------| | `embedding-query` | For search queries/questions | 4096 | | `embedding-passage` | For documents/passages to search | 4096 | ### Document Models | Model | Description | |-------|-------------| | `ocr` | Text extraction with word-level coordinates | | `document-parse` | Convert docs to HTML/Markdown with layout detection | | `document-classify` | Classify documents into user-defined categories | | `information-extract` | Extract structured data with custom JSON schema | | `schema-generate` | Auto-generate extraction schemas from sample docs | | `receipt-extraction` | Prebuilt: extract from receipts | --- ## Model Selection Guide | Your Need | Use This Model | |-----------|---------------| | Complex reasoning, coding | `solar-pro3` with `reasoning_effort: "high"` | | Fast simple responses | `solar-mini` | | Cost-sensitive production | `solar-mini` | | Synthetic data generation | `syn-pro` | | Function calling / tool use | `solar-pro3` (parallel tool calls supported) | | Structured JSON output | `solar-pro3`, `solar-pro2`, or `solar-mini` | | Semantic search (queries) | `embedding-query` | | Semantic search (documents) | `embedding-passage` | | PDF/image → text | `ocr` | | PDF/image → markdown/HTML | `document-parse` | | Extract fields from docs | `information-extract` | | Classify document types | `document-classify` | --- ## API Categories ### 1. Chat Completions **Endpoint**: `POST /v1/chat/completions` - Standard chat, streaming, function calling, structured output, reasoning, prompt caching - OpenAI SDK compatible (just change base_url) - **Details**: Read `references/chat-completions.md` ### 2. Embeddings **Endpoint**: `POST /v1/embeddings` - Dual-model: `embedding-query` for queries, `embedding-passage` for documents - 4096-dimensional normalized vectors (dot product = cosine similarity) - Max 100 texts per batch, 4000 tokens per text - **Details**: Read `references/embeddings.md` ### 3. Document Processing (OCR + Parse + Split) **Endpoints**: `POST /v1/document-digitization`, `POST /v1/document-digitization/async` - OCR: word-level text extraction with bounding boxes - Parse: converts PDF/images to structured HTML/Markdown, chart recognition, equation LaTeX - Sync (≤100 pages) and Async (≤1000 pages) modes - Split: via Classification API with `split=true` - **Details**: Read `references/document-processing.md` ### 4. Information Extraction **Endpoints**: `POST /v1/information-extraction`, `POST /v1/information-extraction/async` - Custom schema-based extraction from documents - Schema Generation: auto-generate schemas from sample docs - Prebuilt models: receipt, air waybill, bill of lading, commercial invoice, KR export declaration - OpenAI SDK compatible (base_url changes to `/v1/information-extraction`) - **Details**: Read `references/information-extraction.md` ### 5. Document Classification **Endpoint**: `POST /v1/document-classification` - Classify into user-defined categories with confidence scores - Document split feature (`split=true`) for multi-doc PDFs - OpenAI SDK compatible (base_url changes to `/v1/document-classification`) - **Details**: Read `references/document-classification.md` ### 6. Agent API (Studio Workflows) **Base URL**: `https://api.upstage.ai/v2` (v2, not v1) - Multi-step workflows configured in Upstage Studio - File upload → Agent job → Poll for results - OpenAI Responses API compatible - **Details**: Read `references/agent-api.md` ### 7. Common Patterns & Error Handling - Error codes, rate limits, retry strategies, SDK setup - RAG pipeline, document routing, batch processing patterns - **Details**: Read `references/common-patterns.md` --- ## Code Generation Guidelines When generating Upstage API code, follow these rules: 1. **Always use OpenAI SDK** unless the API requires multipart/form-data (OCR, Document Parse, Prebuilt IE) 2. **API key from environment**: `os.environ["UPSTAGE_API_KEY"]` — never hardcode 3. **base_url varies by API**: - Chat & Embeddings: `https://api.upstage.ai/v1` - Document Classification: `https://api.upstage.ai/v1/document-classification` - Information Extraction: `https://api.upstage.ai/v1/information-extraction` - Agent API: `https://api.upstage.ai/v2` 4. **Use model aliases** (e.g., `solar-pro3`), not version-specific names 5. **Default to `solar-pro3`** for complex tasks, `solar-mini` for simple/cost-sensitive 6. **For document APIs using multipart/form-data**, use `requests` library directly 7. **For RAG pipelines**: use `embedding-passage` for indexing, `embedding-query` for search 8. **Include error handling**: catch `openai.RateLimitError` with exponential backoff ### Key Differences from OpenAI - Reasoning uses `reasoning_effort` param (not separate reasoning model) - Embeddings use dual-model approach (query vs passage) — not a single model - Document APIs are unique to Upstage (OCR, Parse, IE, Classification) - Structured outputs require `strict: true` and `additionalProperties: false` - IE schemas: first-level properties must be string/integer/number/array (no objects at top level) --- ## Examples - Basic chat: See `examples/chat-example.py` - RAG pipeline: See `examples/rag-example.py` - Document processing: See `examples/document-example.py <path/to/document.pdf>` - Smoke test: Install `requirements.txt`, then run `python scripts/smoke_test.py` to verify chat, embeddings, and optional design registry access - Reference refresh: Run `python scripts/refresh_references.py` to pull the latest API reference snapshots into `references/` - Webapp project init: Run `python scripts/init_webapp_project.py <project-slug>` to create a standard app folder with `README.md`, `.env.example`, and `DEPLOY.md` --- ## Reference Files When you need detailed API parameters, response formats, or advanced features, read the appropriate reference file. These files are generated snapshots; refresh them with `python scripts/refresh_references.py` when you want the latest upstream docs. | File | Content | |------|---------| | `references/chat-completions.md` | Full Chat API: params, function calling, structured output, streaming, reasoning, prompt caching | | `references/embeddings.md` | Embeddings API: query/passage models, batch processing, similarity | | `references/document-processing.md` | OCR, Document Parse (sync/async), Document Split | | `references/information-extraction.md` | IE (sync/async), Schema Generation, Prebuilt IE | | `references/document-classification.md` | Classification API with confidence scores | | `references/agent-api.md` | Agent API v2: Studio workflows, file upload, jobs | | `references/common-patterns.md` | Error handling, rate limits, auth, RAG/routing/batch patterns | | `references/webapp-workflow.md` | Project-root, deployment-provider, and delivery workflow for full webapp tasks | --- ## Source URLs Original sources for reference files. Running `python scripts/refresh_references.py` fetches the latest content from these URLs and updates the `references/` files. | Source | URL | Auth | Updates | |--------|-----|------|---------| | Upstage API Docs | `https://console.upstage.ai/api/docs/for-agents/raw` | None | references/chat-completions.md ~ common-patterns.md (7 files) |

path: config.json
content: projectRoot: /data/.openclaw/workspace/projects
deploymentProvider: vercel
deploymentVisibility: password-protected
defaultWebStack: nextjs-typescript
askOnWebappRequests: yes

path: examples/chat-example.py
content: """Basic Solar chat example using the OpenAI SDK against Upstage."""

from __future__ import annotations

import os

from openai import OpenAI


[REDACTED_SECRET]"UPSTAGE_API_KEY")
if not api_key:
    raise RuntimeError("Set UPSTAGE_API_KEY before running this example.")

client = OpenAI(
    [REDACTED_SECRET],
    base_url="https://api.upstage.ai/v1",
)

# Simple chat
response = client.chat.completions.create(
    model="solar-pro3",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "What are the benefits of solar energy?"},
    ],
    temperature=0.7,
    max_tokens=500,
)

print(response.choices[0].message.content)

# With reasoning enabled
response = client.chat.completions.create(
    model="solar-pro3",
    messages=[{"role": "user", "content": "What is 15% of 80?"}],
    extra_body={"reasoning_effort": "high"},
)

msg = response.choices[0].message
if getattr(msg, "reasoning", None):
    print(f"Reasoning: {msg.reasoning}")
print(f"Answer: {msg.content}")


path: examples/document-example.py
content: """Document Parse + Information Extraction example.

Usage:
  python examples/document-example.py path/to/sample.pdf
"""

from __future__ import annotations

import base64
import json
import os
import sys
from pathlib import Path

import requests
from openai import OpenAI


[REDACTED_SECRET]"UPSTAGE_API_KEY")
if not api_key:
    raise RuntimeError("Set UPSTAGE_API_KEY before running this example.")

if len(sys.argv) < 2:
    raise SystemExit("Usage: python examples/document-example.py path/to/sample.pdf")

document_path = Path(sys.argv[1]).expanduser().resolve()
if not document_path.exists():
    raise FileNotFoundError(f"Document not found: {document_path}")

# --- Step 1: Parse document to Markdown ---
with document_path.open("rb") as f:
    parse_response = requests.post(
        "https://api.upstage.ai/v1/document-digitization",
        headers={"Authorization": f"Bearer {api_key}"},
        files={"document": f},
        data={
            "model": "document-parse",
            "output_formats": '["markdown"]',
        },
        timeout=120,
    )
parse_response.raise_for_status()
parsed = parse_response.json()
markdown_content = parsed.get("content", {}).get("markdown", "")
print(f"Parsed pages: {parsed.get('numBilledPages', 'unknown')}")
print(f"Content preview: {markdown_content[:200]}...")

# --- Step 2: Extract structured data with IE ---
ie_client = OpenAI(
    [REDACTED_SECRET],
    base_url="https://api.upstage.ai/v1/information-extraction",
)


def encode_file(path: Path) -> str:
    with path.open("rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")


base64_data = encode_file(document_path)

extraction = ie_client.chat.completions.create(
    model="information-extract",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:application/octet-stream;base64,{base64_data}"
                    },
                }
            ],
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "invoice_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "invoice_number": {
                        "type": "string",
                        "description": "Invoice number",
                    },
                    "date": {
                        "type": "string",
                        "description": "Invoice date",
                    },
                    "total_amount": {
                        "type": "string",
                        "description": "Total amount with currency",
                    },
                    "vendor_name": {
                        "type": "string",
                        "description": "Name of the vendor/company",
                    },
                    "line_items": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "description": {"type": "string"},
                                "amount": {"type": "string"},
                            },
                            "required": ["description", "amount"],
                            "additionalProperties": False,
                        },
                    },
                },
                "required": ["invoice_number", "total_amount"],
                "additionalProperties": False,
            },
            "strict": True,
        },
    },
)

result = json.loads(extraction.choices[0].message.content)
print(f"\nExtracted data: {json.dumps(result, indent=2)}")


path: examples/rag-example.py
content: """RAG pipeline example: embeddings + retrieval + chat with Upstage APIs."""

from __future__ import annotations

import os

import numpy as np
from openai import OpenAI


[REDACTED_SECRET]"UPSTAGE_API_KEY")
if not api_key:
    raise RuntimeError("Set UPSTAGE_API_KEY before running this example.")

client = OpenAI(
    [REDACTED_SECRET],
    base_url="https://api.upstage.ai/v1",
)

# Sample documents to index
documents = [
    "Solar energy converts sunlight into electricity using photovoltaic cells.",
    "Wind turbines generate power by converting kinetic energy from wind.",
    "Hydroelectric power uses flowing water to spin turbines and produce electricity.",
    "Nuclear energy is produced through fission of uranium atoms in reactors.",
    "Geothermal energy harnesses heat from the Earth's interior for power generation.",
]

# Step 1: Index documents with embedding-passage
response = client.embeddings.create(model="embedding-passage", input=documents)
doc_embeddings = [item.embedding for item in response.data]


# Step 2: Search with embedding-query
def search(query: str, top_k: int = 3) -> list[str]:
    query_resp = client.embeddings.create(model="embedding-query", input=query)
    query_emb = query_resp.data[0].embedding

    scores = [float(np.dot(query_emb, emb)) for emb in doc_embeddings]
    top_indices = np.argsort(scores)[-top_k:][::-1]
    return [documents[i] for i in top_indices]


# Step 3: Generate answer with Solar Pro3
query = "How does solar power work?"
relevant_docs = search(query)

response = client.chat.completions.create(
    model="solar-pro3",
    messages=[
        {
            "role": "system",
            "content": "Answer based on this context:\n\n"
            + "\n".join(f"- {doc}" for doc in relevant_docs),
        },
        {"role": "user", "content": query},
    ],
)

print(f"Query: {query}")
print(f"Retrieved: {relevant_docs}")
print(f"Answer: {response.choices[0].message.content}")


path: references/agent-api.md
content: <!-- Source: https://console.upstage.ai/api/docs/for-agents/raw -->
# Agent API

## 9. Agent API

Agents are reusable, multi-step workflows (e.g., Parse, Classify, Extract) designed to run complex document processing tasks. Create and configure Agents in [Upstage Studio](https://studio.upstage.ai), then execute them via the API.

> **Prerequisite:** Agent API requires creating and configuring an Agent in [Upstage Studio](https://studio.upstage.ai) before making API calls. There is no API for agent creation — the Studio UI is mandatory. Create your agent workflow (e.g., Parse → Classify → Extract), save it, and copy the Agent ID from the Code panel.

**Base URL:** `https://api.upstage.ai/v2` (Note: Agents use v2, not v1)

The Agent API follows the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) format and is compatible with the OpenAI SDK.

### 9.1 Files API (Agent)

Upload, list, retrieve, and delete files used by Agents.

#### Upload File

```
POST https://api.upstage.ai/v2/files
```

**Request Body (multipart/form-data):**

| Parameter | Type   | Required | Description                        |
| --------- | ------ | -------- | ---------------------------------- |
| `file`    | file   | Yes      | The document file to upload        |
| `purpose` | string | No       | File purpose. Default: `user_data` |

**curl:**

```bash
curl -X POST https://api.upstage.ai/v2/files \
  -H "[REDACTED_SECRET]" \
  -F "file=@document.pdf" \
  -F "purpose=user_data"
```

**Python (OpenAI SDK):**

```python
from openai import OpenAI

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v2"
)

with open("document.pdf", "rb") as f:
    file = client.files.create(
        file=f,
        purpose="user_data"
    )

print(file.model_dump())
```

**Response:**

```json
{
  "id": "file-abc123",
  "object": "file",
  "bytes": 17408,
  "created_at": 1756368389,
  "expires_at": null,
  "filename": "document.pdf",
  "purpose": "user_data"
}
```

#### Retrieve File

```
GET https://api.upstage.ai/v2/files/{file_id}
```

#### List Files

```
GET https://api.upstage.ai/v2/files
```

#### Delete File

```
DELETE https://api.upstage.ai/v2/files/{file_id}
```

### 9.2 Jobs API (Agent)

Execute Agents and retrieve results.

#### Create Job

```
POST https://api.upstage.ai/v2/responses
```

**Request Body (JSON):**

| Parameter | Type   | Required | Description                                                               |
| --------- | ------ | -------- | ------------------------------------------------------------------------- |
| `model`   | string | Yes      | Agent ID (e.g., `agt_BxcRatEWzVYH2yRNtyWynn`). Obtain from Upstage Studio |
| `input`   | array  | Yes      | Array of input messages with file references                              |
| `include` | array  | No       | Output scope: `["last"]` for final step only, `["all"]` for all steps     |

**Input Message Format:**

```json
{
  "input": [
    {
      "role": "user",
      "content": [
        {
          "type": "input_file",
          "file_id": "file-abc123"
        }
      ]
    }
  ]
}
```

**curl:**

```bash
curl -X POST https://api.upstage.ai/v2/responses \
  -H "[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "agt_BxcRatEWzVYH2yRNtyWynn",
    "include": ["last"],
    "input": [
      {
        "role": "user",
        "content": [
          {
            "type": "input_file",
            "file_id": "file-abc123"
          }
        ]
      }
    ]
  }'
```

**Python (OpenAI SDK):**

```python
from openai import OpenAI
from time import sleep
import json

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v2"
)

# Upload file
with open("document.pdf", "rb") as f:
    file = client.files.create(file=f, purpose="user_data")

# Create a job
resp = client.responses.create(
    model="agt_BxcRatEWzVYH2yRNtyWynn",
    include=["last"],
    input=[
        {
            "role": "user",
            "content": [{"type": "input_file", "file_id": file.id}]
        }
    ],
)

# Poll until the job completes
while resp.status in {"queued", "in_progress"}:
    sleep(2)
    resp = client.responses.retrieve(resp.id, include=["last"])

# Read the final output
if resp.status == "completed":
    print(json.loads(resp.output_text))
```

#### Retrieve Job

```
GET https://api.upstage.ai/v2/responses/{job_id}
```

| Query Parameter | Type  | Description                                                      |
| --------------- | ----- | ---------------------------------------------------------------- |
| `include[]`     | array | `last` (default) for final step only, `all` for all step results |

**curl:**

```bash
curl https://api.upstage.ai/v2/responses/{JOB_ID}?include[]=last \
  -H "[REDACTED_SECRET]"
```

**Python:**

```python
response = client.responses.retrieve(
    job_id,
    include=["all"]
)
print(response.model_dump())
```

#### Job Response Format

```json
{
  "id": "job_7yMqwGy3rfh6Pwo7MdseMo",
  "object": "response",
  "created_at": 1767613535,
  "status": "completed",
  "model": "agt_BxcRatEWzVYH2yRNtyWynn",
  "output": [
    {
      "id": "fb543069-1a45-4fde-94de-2383995d7339",
      "type": "message",
      "status": "completed",
      "role": "assistant",
      "model": "step_2_extract",
      "content": [
        {
          "type": "output_text",
          "text": "{\"key\": \"value\"}",
          "annotations": [],
          "additional_values": "{...}"
        }
      ]
    }
  ],
  "usage": {
    "input_tokens": 0,
    "output_tokens": 0,
    "total_tokens": 0
  }
}
```

**Response Fields:**

| Field                                  | Type   | Description                                                                               |
| -------------------------------------- | ------ | ----------------------------------------------------------------------------------------- |
| `id`                                   | string | Job ID                                                                                    |
| `status`                               | string | `queued`, `in_progress`, `completed`, or `failed`                                         |
| `output`                               | array  | Array of step results (filtered by `include` parameter)                                   |
| `output[].model`                       | string | Step name (e.g., `step_1_parse`, `step_2_extract`)                                        |
| `output[].content[].text`              | string | Main result text (matches the corresponding standalone API)                               |
| `output[].content[].additional_values` | string | Stringified JSON with extra data (e.g., confidence, locations). Parse with `json.loads()` |

#### Accessing Results via `output_text`

For quick access to the final result, use `include=["last"]` and read `response.output_text`:

```python
response = client.responses.retrieve(job_id, include=["last"])
print(response.output_text)
```

---



path: references/chat-completions.md
content: <!-- Source: https://console.upstage.ai/api/docs/for-agents/raw -->
# Chat Completions API

## 1. Chat Completions API

Generate conversational responses using Solar LLM models.

### Endpoint

```
POST https://api.upstage.ai/v1/chat/completions
```

### Available Models

| Model Alias          | Currently Points To | Description                                                |
| -------------------- | ------------------- | ---------------------------------------------------------- |
| `solar-pro3`         | `solar-pro3-260126` | Latest flagship model (102B MoE, 12B active, 128K context) |
| `solar-pro2`         | `solar-pro2-251215` | Previous generation flagship (31B, 65K context)            |
| `solar-mini`         | `solar-mini-250422` | Lightweight, fast model (10.7B, 32K context)               |
| `syn-pro`            | `syn-pro-251021`    | Synthetic data optimized (no function calling)             |
| `solar-pro2-nightly` | -                   | Experimental nightly (not for production)                  |
| `solar-mini-nightly` | -                   | Experimental nightly (not for production)                  |

### Request Body (JSON)

| Parameter             | Type          | Required | Description                                                                                                                                                            |
| --------------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`               | string        | Yes      | Model name: `solar-pro3`, `solar-pro2`, `solar-mini`, or `syn-pro`                                                                                                     |
| `messages`            | array         | Yes      | Array of message objects with `role` and `content`                                                                                                                     |
| `max_tokens`          | integer       | No       | Maximum tokens to generate                                                                                                                                             |
| `temperature`         | float         | No       | Sampling temperature (0-2). Default: 0.8 for solar-pro3, 0.7 for other models                                                                                          |
| `top_p`               | float         | No       | Nucleus sampling (0-1). Default: 0.95 for solar-pro3, 1.0 for other models                                                                                             |
| `stream`              | boolean       | No       | Enable streaming. Default: false                                                                                                                                       |
| `frequency_penalty`   | float         | No       | Penalty for token frequency (-2 to 2). Default: 1.1                                                                                                                    |
| `presence_penalty`    | float         | No       | Penalty for token presence (-2 to 2). Default: 0.0                                                                                                                     |
| `reasoning_effort`    | string        | No       | Reasoning level: `minimal`, `low`, `medium`, `high`. Default: `minimal`. See notes below                                                                               |
| `tools`               | array         | No       | Function definitions for function calling (max 128 functions)                                                                                                          |
| `tool_choice`         | string/object | No       | Tool selection: `none`, `auto`, `required`, or specific function                                                                                                       |
| `parallel_tool_calls` | boolean       | No       | Enable parallel function calls (solar-pro3 only). Default: true                                                                                                        |
| `response_format`     | object        | No       | For structured outputs (all solar models: solar-pro3, solar-pro2, solar-mini, solar-pro2-nightly, solar-mini-nightly): `{"type": "json_schema", "json_schema": {...}}` |
| `prompt_cache_key`    | string        | No       | Unique key for prompt caching                                                                                                                                          |

### Reasoning Effort

#### Solar Pro 3

`solar-pro3` introduces a dynamic reasoning budget. The budget is dynamically adjusted based on the remaining context window.

| Value    | Reasoning | Ratio | Max Budget | Min Budget | Description                               |
| -------- | --------- | ----- | ---------- | ---------- | ----------------------------------------- |
| `high`   | ON        | 60%   | 32,768     | 8,192      | Maximum reasoning, best for complex tasks |
| `medium` | ON        | 30%   | 16,384     | 4,096      | Balanced reasoning (default)              |
| `low`    | OFF       | -     | -          | -          | No reasoning, fastest response            |

#### Solar Pro 2

For `solar-pro2` and `solar-pro2-nightly`, reasoning is disabled by default (`minimal`). To enable reasoning, set `reasoning_effort` to `high`.

| Value     | Reasoning | Description            |
| --------- | --------- | ---------------------- |
| `high`    | ON        | Enable reasoning       |
| `medium`  | ON        | Treated as `high`      |
| `low`     | OFF       | Treated as `minimal`   |
| `minimal` | OFF       | No reasoning (default) |

**Notes:**

- `solar-mini` and `syn-pro` do not support reasoning. The `reasoning_effort` parameter is ignored for these models.

**Reasoning Budget Calculation (solar-pro3):**

The reasoning budget limits how many tokens the model can consume for reasoning. It is dynamically adjusted:

```
budget = min(max_budget, max(min_budget, ratio * remaining_context))
remaining_context = context_length - input_tokens (or max_tokens if set)
```

### Message Object

**Standard Message:**

```json
{
  "role": "system" | "user" | "assistant",
  "content": "message text"
}
```

**Tool Response Message (for function calling):**

```json
{
  "tool_call_id": "call_abc123",
  "role": "tool",
  "name": "function_name",
  "content": "function_response_json_string"
}
```

When responding to a function call, you must include `tool_call_id` (matching the id from the model's tool_call) and `name` (the function name).

### Example Request

```bash
curl -X POST https://api.upstage.ai/v1/chat/completions \
  -H "[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "solar-pro3",
    "messages": [
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "What is the capital of France?"}
    ],
    "max_tokens": 200,
    "temperature": 0.8
  }'
```

### Example Response (Non-streaming)

```json
{
  "id": "e1a90437-df41-45cd-acc6-a7bacbdd2a86",
  "object": "chat.completion",
  "created": 1723911735,
  "model": "solar-pro3",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The capital of France is Paris."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 8,
    "total_tokens": 33
  }
}
```

### Example Response (With Reasoning)

When `reasoning_effort` is set to `high` or `medium` for solar-pro3, the response includes a `reasoning` field showing the model's thought process:

```json
{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1747209370,
  "model": "solar-pro3",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "reasoning": "The user wants to calculate 15% of 80.\n1. Convert 15% to decimal: 15/100 = 0.15\n2. Multiply: 0.15 × 80 = 12\nThe answer is 12.",
        "content": "15% of 80 is **12**."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 37,
    "completion_tokens": 133,
    "total_tokens": 170,
    "completion_tokens_details": {
      "reasoning_tokens": 45
    }
  }
}
```

**Reasoning Response Fields:**

- `message.reasoning`: The model's internal chain-of-thought reasoning (only present when reasoning is enabled)
- `completion_tokens_details.reasoning_tokens`: Number of tokens consumed by the reasoning process (included in `completion_tokens` total)

### Python Example

```python
import requests

response = requests.post(
    "https://api.upstage.ai/v1/chat/completions",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json"
    },
    json={
        "model": "solar-pro3",
        "messages": [
            {"role": "user", "content": "Hello, how are you?"}
        ]
    }
)
result = response.json()
print(result["choices"][0]["message"]["content"])
```

### OpenAI SDK Compatible

```python
from openai import OpenAI

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v1"
)

response = client.chat.completions.create(
    model="solar-pro3",
    messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content)
```

### Parameter Tips

| Use Case                | Recommended Settings                            |
| ----------------------- | ----------------------------------------------- |
| Complex reasoning tasks | `reasoning_effort: "high"`, `temperature: 0.7`  |
| Fast simple responses   | `reasoning_effort: "low"`, `max_tokens: 200`    |
| Creative writing        | `temperature: 1.2`, `top_p: 0.95`               |
| Consistent outputs      | `temperature: 0.3`, `frequency_penalty: 1.5`    |
| Function calling        | `tools: [...]`, `tool_choice: "auto"`           |
| Structured JSON output  | `response_format: {"type": "json_schema", ...}` |
| Long conversations      | `prompt_cache_key: "unique_session_id"`         |

### Prompt Caching

Prompt caching allows you to reuse previously processed prompts, reducing latency and costs for repeated or similar requests.

**How it works:**

- Set a unique `prompt_cache_key` for each conversation session or context
- When the same key is used, the model can reuse cached prompt processing
- Best suited for multi-turn conversations or repeated prompts with the same prefix

**Best practices:**

- Use a consistent, unique key per user session (e.g., hashed user ID + session ID)
- Keep the cached portion (system prompt, context) at the beginning of messages
- Varying content should come after the cached prefix

### Function Calling

Function calling enables your model to interact with external services such as APIs, databases, or custom functions. The model generates function signatures in JSON format based on your tool definitions.

#### Tool Definition Schema

```json
{
  "type": "function",
  "function": {
    "name": "function_name",
    "description": "Description of what the function does",
    "parameters": {
      "type": "object",
      "properties": {
        "param1": {
          "type": "string",
          "description": "Parameter description"
        },
        "param2": {
          "type": "string",
          "enum": ["option1", "option2"]
        }
      },
      "required": ["param1"]
    }
  }
}
```

**Function Name Rules:**

- Allowed characters: `a-z`, `A-Z`, `0-9`, underscore (`_`), and hyphen (`-`)
- Maximum length: 64 characters
- Pattern: `^[a-zA-Z0-9_-]+$`

#### tool_choice Options

| Value                                                         | Description                                       |
| ------------------------------------------------------------- | ------------------------------------------------- |
| `"none"`                                                      | Model will not call any function                  |
| `"auto"`                                                      | Model decides whether to call functions (default) |
| `"required"`                                                  | Model must call at least one function             |
| `{"type": "function", "function": {"name": "function_name"}}` | Force specific function                           |

#### Parallel Tool Calls (solar-pro3 only)

When `parallel_tool_calls=True` (default), the model may return multiple tool calls in a single response for independent operations (e.g., fetching weather for multiple cities simultaneously).

#### Complete Function Calling Example

```python
from openai import OpenAI
import json

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v1"
)

# Step 1: Define your function
def get_current_weather(location, unit="celsius"):
    """Get the current weather in a given location"""
    # In production, call actual weather API
    weather_data = {
        "seoul": {"temperature": "10", "condition": "cloudy"},
        "san francisco": {"temperature": "18", "condition": "sunny"},
        "paris": {"temperature": "15", "condition": "rainy"}
    }
    city = location.lower()
    if city in weather_data:
        return json.dumps({"location": location, **weather_data[city], "unit": unit})
    return json.dumps({"location": location, "temperature": "unknown"})

# Step 2: Define tools for the model
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "Get the current weather in a given location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city name, e.g. Seoul, San Francisco"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"]
                    }
                },
                "required": ["location"]
            }
        }
    }
]

# Step 3: Initial request
messages = [{"role": "user", "content": "What's the weather in Seoul and Paris?"}]

response = client.chat.completions.create(
    model="solar-pro3",
    messages=messages,
    tools=tools,
    tool_choice="auto",
    parallel_tool_calls=True  # Enable parallel calls
)

# Step 4: Process tool calls
response_message = response.choices[0].message
tool_calls = response_message.tool_calls

if tool_calls:
    messages.append(response_message)

    # Execute each function call
    for tool_call in tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)

        # Call the actual function
        function_response = get_current_weather(
            location=function_args.get("location"),
            unit=function_args.get("unit", "celsius")
        )

        # Append function result to messages
        messages.append({
            "tool_call_id": tool_call.id,
            "role": "tool",
            "name": function_name,
            "content": function_response
        })

    # Step 5: Get final response with function results
    final_response = client.chat.completions.create(
        model="solar-pro3",
        messages=messages
    )
    print(final_response.choices[0].message.content)
```

#### Function Calling Response Structure

When the model decides to call functions, the response includes `tool_calls`:

```json
{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "get_current_weather",
              "arguments": "{\"location\": \"Seoul\", \"unit\": \"celsius\"}"
            }
          },
          {
            "id": "call_def456",
            "type": "function",
            "function": {
              "name": "get_current_weather",
              "arguments": "{\"location\": \"Paris\", \"unit\": \"celsius\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}
```

### Structured Outputs

Generate JSON responses conforming to a specified schema using `response_format`.

#### Schema Requirements

- Supported types: `string`, `number`, `boolean`, `integer`, `object`, `array`
- All fields must be in the `required` array
- Maximum nesting depth: 3 levels
- `strict` must be `true`
- `additionalProperties` must be `false`
- Recursive schemas are not supported
- Definitions for subschemas (`$ref` to local definitions) are not supported

#### Structured Output Example

```python
from openai import OpenAI

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v1"
)

response = client.chat.completions.create(
    model="solar-pro3",
    messages=[
        {"role": "user", "content": "Extract menu items from: Coffee $5, Sandwich $8, Salad $7"}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "menu_extraction",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "items": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "name": {"type": "string"},
                                "price": {"type": "number"}
                            },
                            "required": ["name", "price"],
                            "additionalProperties": False
                        }
                    },
                    "total_items": {"type": "integer"}
                },
                "required": ["items", "total_items"],
                "additionalProperties": False
            }
        }
    }
)

import json
result = json.loads(response.choices[0].message.content)
print(result)
# {"items": [{"name": "Coffee", "price": 5}, {"name": "Sandwich", "price": 8}, {"name": "Salad", "price": 7}], "total_items": 3}
```

### Streaming Response

When `stream=True`, the response is sent as Server-Sent Events (SSE).

#### Streaming Response Structure

Each chunk is a `chat.completion.chunk` object:

```json
{
  "id": "chatcmpl-123",
  "object": "chat.completion.chunk",
  "created": 1723911735,
  "model": "solar-pro3",
  "choices": [
    {
      "index": 0,
      "delta": {
        "role": "assistant",
        "content": "Hello"
      },
      "finish_reason": null
    }
  ]
}
```

Key differences from non-streaming:

- Object type: `chat.completion.chunk` (not `chat.completion`)
- Uses `delta` instead of `message`
- `delta.content` contains incremental text
- Final chunk has `finish_reason: "stop"` and empty `delta`

#### Streaming Example

```python
from openai import OpenAI

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v1"
)

stream = client.chat.completions.create(
    model="solar-pro3",
    messages=[{"role": "user", "content": "Tell me a short story"}],
    stream=True
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
```

---



path: references/common-patterns.md
content: <!-- Source: https://console.upstage.ai/api/docs/for-agents/raw -->
# Common Patterns, Errors, and Limits

## Error Handling

All APIs return standard HTTP status codes:

| Code            | Cause                                   | Solutions                                                                                                                                                    |
| --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 200             | Success                                 | -                                                                                                                                                            |
| 400             | Wrong format or data in request body    | **Solar APIs:** Check JSON syntax, required keys, correct types and values. **Document APIs:** Verify file path, format (JPEG/PNG/PDF/etc), and size (<50MB) |
| 401             | Invalid API key                         | Check `[REDACTED_SECRET]` header format. Verify API key is not deleted in Console                                                           |
| 403             | Insufficient credit                     | [Register a payment method](https://console.upstage.ai/billing) or check credit balance                                                                      |
| 404             | Wrong URL path                          | Verify the request path is correct                                                                                                                           |
| 405             | Using `http://` instead of `https://`   | Use `https://` for all requests                                                                                                                              |
| 415             | Unsupported file format                 | Use supported formats: JPEG, PNG, BMP, PDF, TIFF, HEIC, DOCX, PPTX, XLSX, HWP, HWPX                                                                          |
| 422             | Document is corrupted or damaged        | Re-save or re-export the file                                                                                                                                |
| 429             | Too many requests (rate limit exceeded) | Implement retry with exponential backoff. See [rate limits guide](https://console.upstage.ai/docs/guides/rate-limits)                                        |
| 500/502/503/504 | Server error during processing          | Retry after a short delay. If persistent, contact support                                                                                                    |

### Error Response Format

```json
{
  "error": {
    "message": "Error description",
    "type": "error_type",
    "code": "error_code"
  }
}
```

---


---

## Rate Limits

Rate limits vary by API and commitment tier:

| Metric  | Description         | Applies to                     |
| ------- | ------------------- | ------------------------------ |
| **RPS** | Requests per second | Document processing APIs       |
| **RPM** | Requests per minute | Chat and embedding APIs        |
| **TPM** | Tokens per minute   | Text-based models (Solar LLMs) |
| **PPM** | Pages per minute    | Document processing            |

### Default Rate Limits (Tier 0)

| API                         | RPM/RPS | TPM/PPM     |
| --------------------------- | ------- | ----------- |
| Solar Pro 3 / Pro 2         | 100 RPM | 50,000 TPM  |
| Solar Mini                  | 100 RPM | 50,000 TPM  |
| Embeddings                  | 100 RPM | 300,000 TPM |
| Document OCR                | 1 RPS   | 300 PPM     |
| Document Parse (Sync)       | 1 RPS   | 300 PPM     |
| Document Parse (Async)      | 2 RPS   | 1,200 PPM   |
| Information Extract (Sync)  | 1 RPS   | 300 PPM     |
| Information Extract (Async) | 2 RPS   | 1,200 PPM   |
| Document Classify           | 1 RPS   | 300 PPM     |

**Note:** Sync and Async APIs have different rate limits. Async APIs generally allow higher throughput.

For higher rate limits, upgrade your commitment tier or [contact support](https://get.support.upstage.ai/servicedesk/customer/portal/22/create/10367).

### Handling Rate Limits

#### Exponential Backoff

```python
import time
import openai
from openai import OpenAI

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v1"
)

def call_with_retry(messages, max_retries=3):
    """Call API with exponential backoff on rate limit errors"""
    wait_time = 1

    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model="solar-pro3",
                messages=messages
            )
            return response
        except openai.RateLimitError:
            if attempt == max_retries - 1:
                raise
            print(f"Rate limit hit, waiting {wait_time}s...")
            time.sleep(wait_time)
            wait_time *= 2  # Exponential backoff: 1s, 2s, 4s

    return None

# Usage
response = call_with_retry([{"role": "user", "content": "Hello!"}])
```

#### Batch Processing for Embeddings

```python
from openai import OpenAI

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v1"
)

documents = [
    "Document 1 text...",
    "Document 2 text...",
    # ... many documents
]

# Process in batches to avoid rate limits
batch_size = 32  # Max 100, but smaller batches are safer
all_embeddings = []

for i in range(0, len(documents), batch_size):
    batch = documents[i:i + batch_size]
    response = client.embeddings.create(
        model="embedding-passage",
        input=batch
    )
    all_embeddings.extend([item.embedding for item in response.data])
    print(f"Processed {min(i + batch_size, len(documents))}/{len(documents)}")
```

#### Caching Strategy

```python
from openai import OpenAI

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v1"
)

# Simple in-memory cache
embedding_cache = {}

def get_embedding_cached(text):
    """Get embedding with caching to reduce API calls"""
    if text in embedding_cache:
        return embedding_cache[text]

    response = client.embeddings.create(
        model="embedding-query",
        input=text
    )
    embedding = response.data[0].embedding
    embedding_cache[text] = embedding
    return embedding
```

---


---

## Supported Languages

### Solar LLM Models

| Model       | Languages                                   |
| ----------- | ------------------------------------------- |
| Solar Pro 3 | English, Korean, Japanese (fully supported) |
| Solar Pro 2 | English, Korean, Japanese (fully supported) |
| Solar Mini  | English, Korean, Japanese (fully supported) |

### Document Processing (OCR)

| Character Set                       | Support Level |
| ----------------------------------- | ------------- |
| Alphanumeric (Latin)                | Full          |
| Hangul (Korean)                     | Full          |
| Hanja (Korean Chinese characters)   | Full          |
| Katakana (Japanese)                 | Full          |
| Hiragana (Japanese)                 | Full          |
| Hanzi (Simplified Chinese)          | Beta          |
| Kanji (Japanese Chinese characters) | Beta          |

**Note:** Hanja, Hanzi, and Kanji are writing systems based on Chinese characters used in Korean, Chinese, and Japanese respectively.

---


---

## Common Patterns

### Pattern 1: Document to Structured Data Pipeline

```python
# 1. Parse document to get structured content
parse_response = requests.post(
    "https://api.upstage.ai/v1/document-digitization",
    headers={"Authorization": f"Bearer {api_key}"},
    files={"document": open("document.pdf", "rb")},
    data={"model": "document-parse", "output_formats": '["markdown"]'}
)
markdown_content = parse_response.json()["content"]["markdown"]

# 2. Extract specific fields using Information Extraction
# (Or use the parsed markdown directly with Chat API for Q&A)
```

### Pattern 2: RAG Document Ingestion

```python
# 1. Parse documents
parsed = parse_document(file_path)  # Document Parse API

# 2. Split into chunks
chunks = split_into_chunks(parsed["content"]["markdown"])

# 3. Generate embeddings
embeddings = []
for chunk in chunks:
    response = client.embeddings.create(
        model="embedding-passage",
        input=chunk
    )
    embeddings.append(response.data[0].embedding)

# 4. Store in vector database
vector_db.insert(chunks, embeddings)
```

### Pattern 3: Document Routing with Classification

```python
# 1. Classify document type
classification = classify_document(file_path)  # Document Classification API

# 2. Route to appropriate schema based on document type
schema_map = {
    "invoice": invoice_schema,
    "receipt": receipt_schema,
    "contract": contract_schema
}

# 3. Extract with appropriate schema using Information Extraction API
schema = schema_map.get(classification, default_schema)
result = extract_information(file_path, schema)
```

### Pattern 4: Batch Document Processing

```python
# For large batches, use async APIs to avoid rate limits
import asyncio

async def process_documents(files):
    # Submit all jobs
    job_ids = []
    for file in files:
        job_id = submit_async_job(file)
        job_ids.append(job_id)

    # Poll for results
    results = []
    for job_id in job_ids:
        result = await poll_until_complete(job_id)
        results.append(result)

    return results
```

---


---

## Additional Resources

- [Full API Documentation](https://console.upstage.ai/docs)
- [Cookbook Examples](https://github.com/UpstageAI/cookbook)
- [Pricing](https://www.upstage.ai/pricing)
- [Support](https://support.upstage.ai)



path: references/document-classification.md
content: <!-- Source: https://console.upstage.ai/api/docs/for-agents/raw -->
# Document Classification API

## 5. Document Classification API

Classify documents into user-defined categories. This API uses OpenAI SDK compatible interface.

### Endpoint

```
POST https://api.upstage.ai/v1/document-classification
```

**Note:** Use OpenAI SDK with `base_url="https://api.upstage.ai/v1/document-classification"`

### Request Body (JSON)

| Parameter         | Type    | Required | Description                                                                                                                                                                                             |
| ----------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`           | string  | Yes      | Use `document-classify`                                                                                                                                                                                 |
| `messages`        | array   | Yes      | Array with exactly one user message containing one `image_url` content. Only `role: "user"` is allowed.                                                                                                 |
| `response_format` | object  | Yes      | Class definitions with `json_schema`                                                                                                                                                                    |
| `split`           | boolean | No       | Split multi-document files into separate items in `choices`. Default: false                                                                                                                             |
| `split_criteria`  | array   | No       | Additional criteria for splitting. Array of objects with `criterion` (string) and `description` (string). Example: `[{"criterion": "card_id", "description": "The id that indicates each unit card."}]` |

### Limitations

- **Max pages:** 100 pages per file
- **Max file size:** 50MB
- **Max pixels per page:** 200,000,000 pixels (for non-image files, pixels are calculated after converting to images at 150 DPI)
- **Max classes:** 1,000 class definitions
- **Min classes:** 1 class required
- **Duplicate handling:** Classes with duplicate `const` names are ignored

### Available Models

| Model Alias                 | Description                                  |
| --------------------------- | -------------------------------------------- |
| `document-classify`         | Production model for document classification |
| `document-classify-nightly` | Experimental nightly (not for production)    |

### Example Request (curl)

```bash
curl -X POST https://api.upstage.ai/v1/document-classification \
  -H "[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "document-classify",
    "messages": [
      {
        "role": "user",
        "content": [
          {
            "type": "image_url",
            "image_url": {
              "url": "https://example.com/document.jpg"
            }
          }
        ]
      }
    ],
    "response_format": {
      "type": "json_schema",
      "json_schema": {
        "name": "document-classify",
        "schema": {
          "type": "string",
          "oneOf": [
            {"const": "invoice", "description": "Commercial invoice with itemized charges"},
            {"const": "receipt", "description": "Purchase receipt with transaction details"},
            {"const": "contract", "description": "Legal agreement or contract"},
            {"const": "others", "description": "Other document types"}
          ]
        }
      }
    }
  }'
```

### Python Example (OpenAI SDK - Recommended)

```python
import base64
from openai import OpenAI

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v1/document-classification"
)

def encode_img_to_base64(img_path):
    with open(img_path, 'rb') as img_file:
        return base64.b64encode(img_file.read()).decode('utf-8')

filepath = "./document.png"
base64_data = encode_img_to_base64(filepath)

response = client.chat.completions.create(
    model="document-classify",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image_url",
                    "image_url": {"url": f"data:application/octet-stream;base64,{base64_data}"}
                }
            ]
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "document-classify",
            "schema": {
                "type": "string",
                "oneOf": [
                    {"const": "invoice", "description": "Commercial invoice with itemized charges"},
                    {"const": "receipt", "description": "Purchase receipt with transaction details"},
                    {"const": "contract", "description": "Legal agreement or contract"},
                    {"const": "others", "description": "Other document types"}
                ]
            }
        }
    }
)
classification = response.choices[0].message.content
```

### Python Example (Document Split)

When processing a file containing multiple documents of different types (e.g., a PDF with an invoice, ID cards, and a receipt):

```python
# Document Split: Separate multiple documents in a single file
response = client.chat.completions.create(
    model="document-classify",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image_url",
                    "image_url": {"url": f"data:application/octet-stream;base64,{base64_data}"}
                }
            ]
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "document-classify",
            "schema": {
                "type": "string",
                "oneOf": [
                    {"const": "invoice", "description": "Commercial invoice"},
                    {"const": "id_card", "description": "ID card or identification document"},
                    {"const": "receipt", "description": "Purchase receipt"},
                    {"const": "others", "description": "Other document types"}
                ]
            }
        }
    },
    extra_body={
        "split": True,
        "split_criteria": [
            {"criterion": "document_id", "description": "Unique identifier for each document"}
        ]
    }
)

# Process each split document group
for choice in response.choices:
    doc_type = choice.message.content
    if choice.message.tool_calls:
        args = choice.message.tool_calls[0].function.arguments
        if isinstance(args, str):
            import json
            args = json.loads(args)
        pages = args.get("pages", [])
        print(f"Document type: {doc_type}, Pages: {pages}")
```

### Example Response

```json
{
  "id": "iex-AQZoWf2p5j6TO-AE",
  "object": "chat.completion",
  "model": "document-classify-260304",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "invoice"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 951,
    "completion_tokens": 2,
    "total_tokens": 953
  }
}
```

### Example Response (with split=true)

When `split=true`, multiple document groups are returned as separate items in the `choices` array. Each choice includes `pages` and optional `split_criteria_info` in the `tool_calls`:

```json
{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "model": "document-classify-260304",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "invoice",
        "tool_calls": [
          {
            "id": "call_...",
            "type": "function",
            "function": {
              "name": "additional_values",
              "arguments": {
                "document_type": {"_value": "invoice", "confidence_score": 0.99},
                "pages": [1, 2],
                "split_criteria_info": {"document_id": "INV-001"}
              }
            }
          }
        ]
      },
      "finish_reason": "stop"
    },
    {
      "index": 1,
      "message": {
        "role": "assistant",
        "content": "receipt",
        "tool_calls": [
          {
            "id": "call_...",
            "type": "function",
            "function": {
              "name": "additional_values",
              "arguments": {
                "document_type": {"_value": "receipt", "confidence_score": 0.95},
                "pages": [3],
                "split_criteria_info": {"document_id": "REC-001"}
              }
            }
          }
        ]
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 200,
    "completion_tokens": 150,
    "total_tokens": 350
  }
}
```

### Response Processing

```python
# Extract classification result
classification = response.choices[0].message.content  # "invoice"
```

### Getting Confidence Score

The response includes confidence scores via a `tool_calls` function named `additional_values`:

```python
# Get confidence score from tool_calls (function name: additional_values)
if response.choices[0].message.tool_calls:
    tool_call = response.choices[0].message.tool_calls[0]
    # tool_call.function.name == "additional_values"
    # arguments may be a JSON string or dict depending on SDK version
    args = tool_call.function.arguments
    if isinstance(args, str):
        import json
        args = json.loads(args)

    # Structure: {"document_type": {"_value": "invoice", "confidence_score": 1.0}, "pages": [...], "split_criteria_info": {...}}
    confidence = args["document_type"]["confidence_score"]

    # When split=true, also includes:
    # - pages: list of page numbers for this document group
    # - split_criteria_info: values for each split criterion (if split_criteria was provided)
```

### Class Definition Best Practices

Follow these recommendations for better classification accuracy:

| Guideline                        | Description                                                                        |
| -------------------------------- | ---------------------------------------------------------------------------------- |
| **Clear label names**            | Use concrete, non-overlapping names (e.g., `invoice`, `receipt`, `contract`)       |
| **Differentiating descriptions** | State key criteria in 1-2 sentences and add decision rules for borderline cases    |
| **Use examples (optional)**      | Brief include/exclude examples help with ambiguous cases                           |
| **Manage label count**           | Too many labels can confuse the model. Start with core labels and expand gradually |
| **Consistent format**            | Keep label casing and separators (lowercase, hyphen/underscore) consistent         |

**Class Design Restrictions:**

- Maximum 1,000 class definitions
- Minimum 1 class required
- Classes with duplicate `const` names are ignored

---



path: references/embeddings.md
content: <!-- Source: https://console.upstage.ai/api/docs/for-agents/raw -->
# Embeddings API

## 2. Embeddings API

Convert text into vector embeddings for semantic search and similarity.

### Endpoint

```
POST https://api.upstage.ai/v1/embeddings
```

### Request Body (JSON)

| Parameter | Type         | Required | Description                                                            |
| --------- | ------------ | -------- | ---------------------------------------------------------------------- |
| `model`   | string       | Yes      | Use `embedding-query` for queries or `embedding-passage` for documents |
| `input`   | string/array | Yes      | Text to embed. Single string or array of strings                       |

### When to Use Which Model

| Model               | Use Case                       | Input Type                                     |
| ------------------- | ------------------------------ | ---------------------------------------------- |
| `embedding-query`   | User search queries, questions | Short, specific text (questions, search terms) |
| `embedding-passage` | Documents to be searched       | Longer text (paragraphs, document chunks)      |

**Example RAG Pipeline:**

- Index documents with `embedding-passage`
- Embed user query with `embedding-query`
- Find most similar passages using dot product

### Limitations

- **Vector dimension:** 4096
- **Max tokens per input:** 4,000
- **Max batch size:** 100 texts per request
- **Max tokens per batch:** 204,800 total tokens
- **Recommended:** Keep each text under 512 tokens for optimal results
- **Vector normalization:** Outputs normalized vectors with magnitude of 1 (dot product = cosine similarity)

### Example Request

```bash
curl -X POST https://api.upstage.ai/v1/embeddings \
  -H "[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "embedding-query",
    "input": "What is machine learning?"
  }'
```

### Example Response

```json
{
  "object": "list",
  "data": [
    {
      "index": 0,
      "embedding": [0.0012, 0.0287, -0.0045, ...],
      "object": "embedding"
    }
  ],
  "model": "embedding-query",
  "usage": {
    "prompt_tokens": 5,
    "total_tokens": 5
  }
}
```

### Python Example (Batch Processing)

```python
import requests

documents = [
    "Korea is a beautiful country.",
    "Machine learning is a subset of AI.",
    "Python is a great programming language."
]

response = requests.post(
    "https://api.upstage.ai/v1/embeddings",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json"
    },
    json={
        "model": "embedding-passage",
        "input": documents
    }
)
embeddings = [item["embedding"] for item in response.json()["data"]]
```

### OpenAI SDK Compatible

```python
from openai import OpenAI

client = OpenAI(
    [REDACTED_SECRET]",
    base_url="https://api.upstage.ai/v1"
)

# Single text
response = client.embeddings.create(
    model="embedding-query",
    input="What is machine learning?"
)
embedding = response.data[0].embedding

# Batch processing
response = client.embeddings.create(
    model="embedding-passage",
    input=["Text 1", "Text 2", "Text 3"]
)
embeddings = [item.embedding for item in response.data]
```

### Similarity Measurement

Since vectors are normalized (magnitude = 1), dot product equals cosine similarity:

```python
import numpy as np

# Calculate similarity between query and passages
query_embedding = get_embedding(query, model="embedding-query")
passage_embeddings = get_embeddings(passages, model="embedding-passage")

similarity_scores = [np.dot(query_embedding, p) for p in passage_embeddings]
most_similar_idx = np.argmax(similarity_scores)
print(f"Most similar: {passages[most_similar_idx]}")
```

---



path: references/webapp-workflow.md
content: # Webapp Project & Deployment Workflow

Use this reference when the user wants a full web service/app, not just a code snippet.

## Startup Questions

Before creating a new webapp project, confirm these setup choices once if they are not already known:

1. Which deployment system should be used?
   - Preferred default: `vercel`
2. Which project root should be used?
   - Preferred default: `/data/.openclaw/workspace/projects`
3. Which visibility mode should be used?
   - Preferred default: `password-protected`
   - Other possible answers: `private-team-only`, `public`, `code-only`

If the user has already given defaults, use them without asking every time.

## Recommended Defaults

- Project root: `/data/.openclaw/workspace/projects`
- Deployment provider: `vercel`
- Visibility: `password-protected`
- Vercel access model: publicly reachable deployment URL + app-level password gate (do not rely on Vercel login wall by default)
- Web stack: `Next.js + TypeScript`

## Project Layout

Create one folder per app:

- `/data/.openclaw/workspace/projects/<project-slug>/`

Include at minimum:

- `README.md` — what the app does and how to run it
- `.env.example` — required environment variables
- `DEPLOY.md` — deployment notes and target URL pattern

## Delivery Contract

When finishing a webapp task, return all of these if available:

- project path
- stack/framework used
- required environment variables
- deployment method
- visibility mode
- deployment URL or next deployment step
- site password, if password-protected deployment was requested

## Vercel Default

When the deployment provider is `vercel`:

- prefer a Next.js app
- make the app buildable without localhost access
- treat an external preview or production URL as part of the deliverable
- default to password-protected deployments unless the user says otherwise
- if deployment cannot be completed from the current environment, leave a ready-to-deploy project and say exactly what command or UI step remains


path: requirements.txt
content: openai>=1.30.0
numpy>=1.26.0
requests>=2.31.0


path: scripts/init_webapp_project.py
content: #!/usr/bin/env python3
"""Create a standard webapp project folder for upstage-builder work.

Usage:
  python skills/upstage-builder/scripts/init_webapp_project.py <project-slug>
"""

from __future__ import annotations

import json
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CONFIG_PATH = ROOT / "config.json"


def slugify(value: str) -> str:
    value = value.strip().lower()
    value = re.sub(r"[^a-z0-9]+", "-", value)
    value = re.sub(r"-+", "-", value).strip("-")
    if not value:
        raise ValueError("Project slug is empty after normalization")
    return value


def main() -> None:
    if len(sys.argv) < 2:
        raise SystemExit("Usage: python skills/upstage-builder/scripts/init_webapp_project.py <project-slug>")

    raw_slug = sys.argv[1]
    slug = slugify(raw_slug)
    config = json.loads(CONFIG_PATH.read_text())
    project_root = Path(config["projectRoot"])
    provider = config["deploymentProvider"]
    stack = config["defaultWebStack"]

    project_dir = project_root / slug
    project_dir.mkdir(parents=True, exist_ok=True)

    (project_dir / "README.md").write_text(
        f"# {slug}\n\n"
        f"Generated by upstage-builder.\n\n"
        f"- Stack: {stack}\n"
        f"- Deployment: {provider}\n"
        f"- Path: {project_dir}\n"
    )

    (project_dir / ".env.example").write_text(
        "UPSTAGE_API_KEY=\n"
    )

    (project_dir / "DEPLOY.md").write_text(
        f"# Deploy\n\n"
        f"Provider: {provider}\n\n"
        f"Expected outcome: external URL instead of localhost-only preview.\n"
    )

    print(project_dir)


if __name__ == "__main__":
    main()


path: scripts/refresh_references.py
content: #!/usr/bin/env python3
"""Refresh upstage-builder reference files from upstream sources.

Usage:
  python skills/upstage-builder/scripts/refresh_references.py
"""

from __future__ import annotations

import re
from pathlib import Path
from urllib.request import Request, urlopen

ROOT = Path(__file__).resolve().parents[1]
REFERENCE_DIR = ROOT / "references"
API_DOCS_URL = "https://console.upstage.ai/api/docs/for-agents/raw"

SECTION_PATTERNS = {
    "chat-completions.md": [r"^## 1\. Chat Completions API$"],
    "embeddings.md": [r"^## 2\. Embeddings API$"],
    "document-processing.md": [
        r"^## Supported File Formats \(Document APIs\)$",
        r"^## 3\. Document OCR API$",
        r"^## 4\. Document Parse API$",
        r"^## 10\. Document Split API$",
    ],
    "document-classification.md": [r"^## 5\. Document Classification API$"],
    "information-extraction.md": [
        r"^## 6\. Information Extraction API$",
        r"^## 7\. Schema Generation API$",
        r"^## 8\. Prebuilt Information Extraction API$",
    ],
    "agent-api.md": [r"^## 9\. Agent API$"],
    "common-patterns.md": [
        r"^## Error Handling$",
        r"^## Rate Limits$",
        r"^## Supported Languages$",
        r"^## Common Patterns$",
        r"^## Additional Resources$",
    ],
}

TITLES = {
    "chat-completions.md": "Chat Completions API",
    "embeddings.md": "Embeddings API",
    "document-processing.md": "Document Processing APIs",
    "document-classification.md": "Document Classification API",
    "information-extraction.md": "Information Extraction API",
    "agent-api.md": "Agent API",
    "common-patterns.md": "Common Patterns, Errors, and Limits",
}


def fetch_text(url: str) -> str:
    req = Request(url)
    with urlopen(req, timeout=60) as resp:
        return resp.read().decode("utf-8")


def split_sections(raw: str) -> dict[str, str]:
    lines = raw.splitlines()
    heading_idx: list[tuple[int, str]] = []
    for idx, line in enumerate(lines):
        if line.startswith("## "):
            heading_idx.append((idx, line.strip()))

    sections: dict[str, str] = {}
    for i, (start, heading) in enumerate(heading_idx):
        end = heading_idx[i + 1][0] if i + 1 < len(heading_idx) else len(lines)
        sections[heading] = "\n".join(lines[start:end]).strip() + "\n"
    return sections


def build_api_reference_files(raw: str) -> dict[str, str]:
    sections = split_sections(raw)
    outputs: dict[str, str] = {}
    for filename, patterns in SECTION_PATTERNS.items():
        chunks: list[str] = []
        for pattern in patterns:
            rx = re.compile(pattern)
            matched = [body for heading, body in sections.items() if rx.match(heading)]
            if not matched:
                raise RuntimeError(f"Missing expected section for {filename}: {pattern}")
            chunks.extend(matched)
        outputs[filename] = (
            f"<!-- Source: {API_DOCS_URL} -->\n"
            f"# {TITLES[filename]}\n\n"
            + "\n\n---\n\n".join(chunks)
            + "\n"
        )
    return outputs


def main() -> None:
    REFERENCE_DIR.mkdir(parents=True, exist_ok=True)

    api_raw = fetch_text(API_DOCS_URL)
    for filename, content in build_api_reference_files(api_raw).items():
        (REFERENCE_DIR / filename).write_text(content)
        print(f"updated {filename}")


if __name__ == "__main__":
    main()


path: scripts/smoke_test.py
content: """Smoke-test Upstage chat and embeddings access.

Usage:
  python skills/upstage-builder/scripts/smoke_test.py
"""

from __future__ import annotations

import os

from openai import OpenAI


def fail(message: str, code: int = 1) -> None:
    print(f"FAIL: {message}")
    raise SystemExit(code)


[REDACTED_SECRET]"UPSTAGE_API_KEY")
if not api_key:
    fail("UPSTAGE_API_KEY is not set")

client = OpenAI([REDACTED_SECRET], base_url="https://api.upstage.ai/v1")

print("[1/2] Chat completion test...")
chat = client.chat.completions.create(
    model="solar-mini",
    messages=[{"role": "user", "content": "Reply with exactly: OK"}],
    max_tokens=10,
)
reply = (chat.choices[0].message.content or "").strip()
print(f"chat_reply={reply!r}")
if reply != "OK":
    fail(f"unexpected chat reply: {reply!r}")

print("[2/2] Embeddings test...")
emb = client.embeddings.create(model="embedding-query", input="solar energy")
vector = emb.data[0].embedding
print(f"embedding_dims={len(vector)}")
if len(vector) != 4096:
    fail(f"unexpected embedding dimension: {len(vector)}")

print("PASS")


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)