Model API
The Model API is a plain relay. You send the request your SDK already knows how to build, we send it upstream, and we take credits out of your account for what the call actually used.
It is separate from /open/v1. That surface plays cards on behalf of a signed-in user;
this one is for your own code, authenticated with your own key.
Conversation Agent mode does not change this relay: your application still executes tool calls and sends their results back. Server-managed preparation belongs to the conversation API.
Base URL: https://api.harperharbor.com/v1
Getting a key
Section titled “Getting a key”In the console, open Personal API keys, create a key, and tick Call models directly through the API. Then pick the models this key may call, or tick All models to let it call every model, including models added later.
You can also set a monthly credit limit per key. It stops that key once it has spent that much in a calendar month, and it does not affect your other keys.
Both request shapes work
Section titled “Both request shapes work”POST /v1/chat/completions takes an OpenAI-shaped request. POST /v1/messages takes an
Anthropic-shaped one. Either shape works with any model — the conversion happens
upstream, so a Claude-shaped request against a DeepSeek model comes back Claude-shaped.
from openai import OpenAI
client = OpenAI(api_key="hh_live_…", base_url="https://api.harperharbor.com/v1")
stream = client.chat.completions.create( model="deepseek-v4-flash", messages=[{"role": "user", "content": "Write one line about the sea."}], stream=True,)for chunk in stream: print(chunk.choices[0].delta.content or "", end="")from anthropic import Anthropic
client = Anthropic(api_key="hh_live_…", base_url="https://api.harperharbor.com")
message = client.messages.create( model="deepseek-v4-flash", max_tokens=256, messages=[{"role": "user", "content": "Write one line about the sea."}],)print(message.content[0].text)import OpenAI from 'openai'
const client = new OpenAI({ apiKey: 'hh_live_…', baseURL: 'https://api.harperharbor.com/v1' })const res = await client.chat.completions.create({ model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'Write one line about the sea.' }],})console.log(res.choices[0].message.content)curl https://api.harperharbor.com/v1/chat/completions \ -H "Authorization: Bearer hh_live_…" \ -H "Content-Type: application/json" \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}'The Anthropic x-api-key header works too, so an Anthropic SDK needs nothing beyond the
base URL.
Listing what a key can use
Section titled “Listing what a key can use”curl https://api.harperharbor.com/v1/models -H "Authorization: Bearer hh_live_…"{ "object": "list", "data": [ { "id": "deepseek-v3.2-gmicloud", "object": "model", "owned_by": "harperharbor", "type": "chat", "max_input_tokens": 128000, "max_output_tokens": 8192, "pricing": { "unit": "credits_per_million_tokens", "input": 1728.86, "output": 2563.49, "cached_input": 178.85, "cache_write": 2161.08 } } ]}The list contains only the models this key may call, so it doubles as a check that the key is configured the way you think it is.
Every entry has a type: chat for the models on this page, image or video for the
models of the image and video API.
max_input_tokens and max_output_tokens are what we accept and generate, not the
model’s advertised window. They are usually the same thing; where they differ, ours is the
one that applies.
Models and channels
Section titled “Models and channels”One model often runs on several channels, and the price between them can differ several
fold. The model id carries the channel: deepseek-v3.2-gmicloud and
deepseek-v3.2-vertex are the same model bought from different places, at different
prices and with different latency. Pick the id, not just the model — there is no automatic
switching, so the channel you name is the channel you get.
Channel names follow one rule: where we buy at a negotiated rate the channel carries our own codename, and where we resell a public marketplace the channel carries that provider’s name.
What a call costs
Section titled “What a call costs”Prices are credits per million tokens, listed per model and channel in the console and
in /v1/models. A call is charged as:
| Part | Rate |
|---|---|
| Fresh input tokens | input |
| Cache reads | cached_input |
| Cache writes | cache_write |
| Output tokens | output |
Input and output round to the nearest credit, cache reads round up, and a billed call costs at least one credit.
Credits are held when the call starts, based on your prompt plus the maximum output you asked for, and settled against real usage when it finishes. A call that produces nothing costs nothing.
If you disconnect mid-stream we close the upstream connection immediately and charge for what was already generated — so stopping early really does stop the meter.
Errors
Section titled “Errors”Errors come back in the shape of the endpoint you called: OpenAI-shaped from
/v1/chat/completions, Anthropic-shaped from /v1/messages.
| Status | What happened | What to do |
|---|---|---|
| 401 | The key is missing, revoked or expired | Create a new key in the console |
| 403 | The key has no model permission, or the model is not on its list | Add the model to the key, or check /v1/models |
| 404 | No such model | Take the id from /v1/models |
| 402 | Out of credits, or the key hit its monthly limit | Top up, or raise the limit |
| 400 | The request body is rejected upstream | The message is the provider’s own, unchanged |
| 502 | The provider did not answer | Retry, or use another channel of the same model |
Anything the provider says about your request is passed through untouched. Replacing it with our own wording would take away the only thing that tells you what to change.