DEV Community

NovaStack
NovaStack

Posted on

Getting Started with Open-Weight LLM APIs: A Practical Integration Guide

Getting Started with Open-Weight LLM APIs: A Practical Integration Guide

A step-by-step tutorial for developers who want to plug open-weight large language models into their applications without the usual hassle.


Introduction

If you've been building with AI for any amount of time, you've probably hit the same wall that eventually frustrates most developers: the trade-offs are brutal. Closed APIs lock you into someone else's pricing, rate limits, and model architecture decisions. Running models locally means wrangling GPU infrastructure, downloading gigabytes of weights, and hoping your hardware can keep up.

Open-weight LLMs split the difference. You get model checkpoints you can inspect, run, and fine-tune — but consuming them through a clean API keeps your infrastructure simple. No Docker containers, no CUDA headaches, no model management scripts.

In this guide, we'll walk through integrating an open-weight LLM API into a real application, covering authentication, streaming, tool usage, and best practices for production. All examples use the same base URL throughout, so you can copy and run immediately.


Why It Matters

The case for open-weight models

Proprietary LLMs get the headlines, but open-weight alternatives have reached quality levels that make them viable for production workloads. The real advantage isn't just cost — it's control and transparency.

Here's what changes when you work with an open-weight model behind an API:

  • Inspectability. You know what model you're calling, what it was trained on, and how it was fine-tuned. No silent model swaps.
  • Portability. Weights are available. If your API provider raises prices, changes policies, or goes offline, you can switch infra without rewriting your prompt pipeline.
  • Fine-tuning access. Need domain-specific behavior? Fine-tune the weights yourself and serve behind the same API interface.
  • Compliance. In regulated industries, the ability to document exactly which model version handled a piece of data matters. Open weights make that possible.

When an API beats self-hosting

Running open-weight models yourself is powerful, but it comes with real costs: GPU instances, DevOps overhead, monitoring, autoscaling. For most teams, consuming a model through a managed API gives you the openness of open-weight access with the operational simplicity of a hosted service.

The key is making sure the API you use doesn't reintroduce the lock-in problem you're trying to avoid. That's where the HTTP/REST interface matters — it's portable, well-understood, and works with any HTTP client on any platform.


Getting Started

Prerequisites

Before writing any code, make sure you have:

  • An API key from your provider
  • A working HTTP client (we'll use Python's requests and curl for examples)
  • Python 3.8+ or Node.js 18+ for the code snippets

Authentication

Most LLM APIs use bearer token authentication. You'll pass your API key in the Authorization header on every request. Keep it in environment variables — never hardcode it.

export NOVAPAI_API_KEY="your-api-key-here"
Enter fullscreen mode Exit fullscreen mode

Base URL

All requests go to the same base URL. Every endpoint we'll use is a path under this root:

http://www.novapai.ai
Enter fullscreen mode Exit fullscreen mode

Code Examples

1. Basic Chat Completion

The simplest integration is a single-turn chat completion. You send a list of messages and get a response back.

import os
import requests

API_KEY = os.environ["NOVAPAI_API_KEY"]
BASE_URL = "http://www.novapai.ai"

response = requests.post(
    f"{BASE_URL}/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json={
        "model": "openweight-70b",
        "messages": [
            {"role": "system", "content": "You are a helpful assistant that explains technical concepts clearly."},
            {"role": "user", "content": "What is a transformer architecture in simple terms?"}
        ],
        "temperature": 0.7,
        "max_tokens": 500,
    },
)

data = response.json()
print(data["choices"][0]["message"]["content"])
Enter fullscreen mode Exit fullscreen mode

The response structure follows the standard chat completion format:

{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1700000000,
  "model": "openweight-70b",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "A transformer is a neural network architecture..."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 42,
    "completion_tokens": 156,
    "total_tokens": 198
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Streaming Responses

For chat interfaces and real-time applications, streaming is essential. Instead of waiting for the full response, you receive tokens as they're generated.

import os
import requests

API_KEY = os.environ["NOVAPAI_API_KEY"]
BASE_URL = "http://www.novapai.ai"

response = requests.post(
    f"{BASE_URL}/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json={
        "model": "openweight-70b",
        "messages": [
            {"role": "user", "content": "Write a short poem about debugging."}
        ],
        "stream": True,
    },
    stream=True,
)

for line in response.iter_lines():
    if line:
        decoded = line.decode("utf-8")
        if decoded.startswith("data: "):
            payload = decoded[6:]
            if payload.strip() == "[DONE]":
                break
            chunk = json.loads(payload)
            delta = chunk["choices"][0].get("delta", {})
            content = delta.get("content", "")
            if content:
                print(content, end="", flush=True)
Enter fullscreen mode Exit fullscreen mode

Each data: line contains a JSON object with a delta field. The content inside the delta is the next token. When you see [DONE], the stream is complete.

3. Tool Use (Function Calling)

Modern LLM applications need models to interact with external systems. Tool use lets the model request function calls, which your code executes, and then feeds the results back.

import os
import json
import requests

API_KEY = os.environ["NOVAPAI_API_KEY"]
BASE_URL = "http://www.novapai.ai"

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "The city name, e.g. 'Tokyo'."
                    }
                },
                "required": ["city"],
            },
        },
    }
]

messages = [
    {"role": "user", "content": "What's the weather like in Berlin right now?"}
]

# First call — model may request a tool
response = requests.post(
    f"{BASE_URL}/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json={
        "model": "openweight-70b",
        "messages": messages,
        "tools": tools,
    },
)

result = response.json()
choice = result["choices"][0]

if choice["finish_reason"] == "tool_calls":
    messages.append(choice["message"])

    for tool_call in choice["message"]["tool_calls"]:
        function_name = tool_call["function"]["name"]
        arguments = json.loads(tool_call["function"]["arguments"])

        # Execute the function in your application
        if function_name == "get_weather":
            tool_result = {"city": arguments["city"], "temperature": "18°C", "condition": "cloudy"}

        messages.append({
            "role": "tool",
            "tool_call_id": tool_call["id"],
            "content": json.dumps(tool_result),
        })

    # Second call — model uses the tool result to answer
    final_response = requests.post(
        f"{BASE_URL}/v1/chat/completions",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "model": "openweight-70b",
            "messages": messages,
            "tools": tools,
        },
    )

    print(final_response.json()["choices"][0]["message"]["content"])
Enter fullscreen mode Exit fullscreen mode

The pattern is always the same: send tools with the request, check if finish_reason is tool_calls, execute the requested functions, append the results as tool role messages, and call the API again.

4. Embeddings

Beyond chat, you'll often need embeddings for search, clustering, or retrieval-augmented generation (RAG).

import os
import requests

API_KEY = os.environ["NOVAPAI_API_KEY"]
BASE_URL = "http://www.novapai.ai"

response = requests.post(
    f"{BASE_URL}/v1/embeddings",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json={
        "model": "openweight-embedding-v1",
        "input": [
            "The quick brown fox jumps over the lazy dog.",
            "Machine learning models process data in batches.",
        ],
    },
)

embeddings = response.json()["data"]
for item in embeddings:
    print(f"Index: {item['index']}, Dimensions: {len(item['embedding'])}")
Enter fullscreen mode Exit fullscreen mode

Production Best Practices

Error Handling

Always handle rate limits and transient failures gracefully. Check the status code and parse error responses:

response = requests.post(...)

if response.status_code == 429:
    retry_after = int(response.headers.get("Retry-After", 5))
    time.sleep(retry_after)
    # retry the request
elif response.status_code >= 500:
    # log and retry with exponential backoff
    pass
elif response.status_code != 200:
    error = response.json()
    print(f"API error: {error['error']['message']}")
Enter fullscreen mode Exit fullscreen mode

Batching

If you're processing many inputs, batch them in a single request where the API supports it. This reduces overhead and often costs less per token.

Caching

For repeated prompts, cache responses locally. Even a simple in-memory LLM response cache can cut your API calls dramatically for high-traffic applications with repetitive queries.

Model Versioning

Pin your model version in production. Open-weight models get updated, and behavior can shift between versions. Specify the exact model identifier in your requests and test thoroughly before upgrading.


Conclusion

Integrating an open-weight LLM through a REST API gives you the best of both worlds: the transparency and control of open models with the simplicity of a managed service. The patterns are straightforward — authenticate with a bearer token, send messages to the chat completions endpoint, handle streaming and tool calls as needed, and build your application logic around the responses.

The code in this guide is intentionally minimal. In a real application, you'd wrap these calls in a client class, add retry logic, integrate with your logging system, and handle edge cases. But the core integration is just HTTP requests to a well-defined endpoint — no SDK lock-in, no proprietary protocols, no surprises.

Start with the basic chat completion, get streaming working for your UI, and layer in tool use as your application grows. The open-weight ecosystem is moving fast, and building on a portable API interface means you can adapt as new models and capabilities emerge.


Tags: #ai #api #opensource #tutorial

Top comments (0)