Kimi K3, Moonshot AI’s 2.8-trillion-parameter mixture-of-experts model, is now cataloged on NativePort’s Hugging Face route as huggingface/moonshotai/Kimi-K3, pinned to the fireworks-ai serving backend. Because the route speaks OpenAI’s chat-completions contract, you don’t need Moonshot’s SDK or a Hugging Face token. The standard openai Python package works as-is once you point its base_url at the gateway. This tutorial covers a basic call, checking what the model actually supports before you rely on it, vision input, tool calls, and the errors this specific setup produces.
What you’ll need
- A NativePort API key (sign up; $5 of credit is seeded automatically).
pip install openai: the official OpenAI Python SDK, used purely as an HTTP client here; no OpenAI account or key involved.- Your key exported as an environment variable, never hardcoded:
export NATIVEPORT_API_KEY="np_..."
Two ways to reach the same model
NativePort exposes Kimi K3 through two paths that end up calling the same pinned backend:
- Unified endpoint (this tutorial):
POST https://api.nativeport.ai/inference/v1/chat/completions, modelhuggingface/moonshotai/Kimi-K3. This is the canonical id. The same request shape works for OpenAI, Anthropic, Grok and Hugging Face models by swapping themodelstring, and only the first/after the provider prefix is significant, so everything afterhuggingface/is Hugging Face’s own model id, slashes included. - Direct Hugging Face passthrough:
POST https://api.nativeport.ai/huggingface/v1/chat/completions, modelmoonshotai/Kimi-K3(bare, resolves to the cataloged default provider) ormoonshotai/Kimi-K3:fireworks-ai(pinned explicitly). Use this if you want Hugging Face’s own response shape untouched by the unified layer, or want to pin the serving provider yourself rather than trust the default.
Both currently resolve to the same fireworks-ai backend. The examples below use the unified endpoint since it’s what makes the openai SDK usable unmodified.
A basic call
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["NATIVEPORT_API_KEY"],
base_url="https://api.nativeport.ai/inference/v1",
)
response = client.chat.completions.create(
model="huggingface/moonshotai/Kimi-K3",
messages=[{"role": "user", "content": "In one sentence, what is a mixture-of-experts model?"}],
)
print(response.choices[0].message.content)
The response comes back shaped exactly like chat.completions.create returns for any OpenAI-compatible model: choices[0].message.content, usage.prompt_tokens/completion_tokens/total_tokens, finish_reason. Nothing Hugging-Face-specific leaks through at this endpoint.
Check what the model supports before you rely on it
Rather than take this article’s word for Kimi K3’s capabilities, ask the gateway directly. It’s the same lookup you’d run for any model before writing code against it:
import os
import urllib.request
import json
req = urllib.request.Request(
"https://api.nativeport.ai/inference/v1/models/huggingface/moonshotai/Kimi-K3"
)
req.add_header("Authorization", f"Bearer {os.environ['NATIVEPORT_API_KEY']}")
with urllib.request.urlopen(req, timeout=30) as resp:
print(json.dumps(json.loads(resp.read()), indent=2))
This returns a capabilities object (streaming, tools, tool_choice, vision, all true for this model), a supported_parameters array, and pricing. response_format (structured output / JSON mode) never appears in supported_parameters for any model on this endpoint. It’s outside the unified /inference API’s common field set by design (the endpoint only admits the field intersection all four backend providers can express), not a Kimi-K3-specific gap. Sending it here fails with 400 unsupported_parameter regardless of what the underlying model supports upstream. If your workload depends on schema-constrained output, use tool calls with a single required function instead (see below) rather than response_format.
Vision input
Kimi K3’s catalog entry reports capabilities.vision: true (sourced from the Hugging Face router’s own architecture.input_modalities for this model), so image_url content parts pass the gateway’s structural validation instead of being rejected outright. The route accepts the image and returns a model-generated description of its contents.
response = client.chat.completions.create(
model="huggingface/moonshotai/Kimi-K3",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{"type": "image_url", "image_url": {"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google_2015_logo.svg/500px-Google_2015_logo.svg.png"}},
],
}],
max_tokens=600,
)
print(response.choices[0].message.content)
Set max_tokens explicitly. A default that’s too low can truncate the response before it finishes describing the image; 600 is comfortable headroom for a normal description.
Sending image_url content to a model whose catalog entry reports capabilities.vision: false fails with 400 unsupported_parameter. The gateway validates this structurally for every model, so the failure mode is the same regardless of provider.
Tool calls
Kimi K3’s catalog entry also reports capabilities.tools: true and tool_choice: true. This works end to end too: forcing the call with tool_choice="required" returns a structurally valid tool call and finish_reason: "tool_calls".
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Look up the current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
response = client.chat.completions.create(
model="huggingface/moonshotai/Kimi-K3",
messages=[{"role": "user", "content": "What's the weather in Boston?"}],
tools=tools,
tool_choice="required",
)
call = response.choices[0].message.tool_calls[0]
print(call.function.name, call.function.arguments)
tool_choice="required" forces a function call instead of leaving the choice to the model. That’s useful here for a deterministic example, and worth reaching for in production whenever your code path can’t do anything with a plain-text answer instead of a call.
tools must be type: "function". The gateway rejects other tool types before the request reaches the backing provider.
Errors you’ll actually hit
The openai SDK raises openai.APIStatusError subclasses keyed to HTTP status. Catch the specific ones the gateway documents, then fall back to the general case:
import openai
try:
response = client.chat.completions.create(
model="huggingface/moonshotai/Kimi-K3",
messages=[{"role": "user", "content": "Hello"}],
)
except openai.AuthenticationError:
raise SystemExit("401 — NATIVEPORT_API_KEY is missing, malformed, or revoked.")
except openai.PermissionDeniedError:
raise SystemExit("403 — the account behind this key isn't active (e.g. suspended).")
except openai.NotFoundError:
raise SystemExit("404 — model_not_found: check the model id for typos.")
except openai.RateLimitError:
raise SystemExit("429 — gateway_rate_limited: back off and retry.")
except openai.BadRequestError as e:
raise SystemExit(f"400 — {e.body.get('code', 'invalid request')}: {e.message}")
except openai.APIStatusError as e:
if e.status_code == 402:
raise SystemExit("402 — NativePort balance is $0. Every request fails closed instead of degrading silently; top up and retry.")
raise
A few specifics worth knowing before they surprise you:
401comes back as{"error": "Unauthorized."}, a plain string, not the nestederror.codeobject the 400/402/429 cases use.403is also a plain string body, an account-status problem (e.g. suspended), not a credentials problem, so retrying with the same key won’t help.404model_not_found: Kimi K3 was only just added to the Hugging Face route’s catalog. If you hit this immediately after reading about it here, it’s worth a retry after a moment rather than assuming the id is wrong; if it persists, double-checkhuggingface/moonshotai/Kimi-K3for typos.502means the upstream provider itself was unreachable, a Fireworks-side problem, not a NativePort or Kimi K3 problem specifically.
What this costs
Kimi K3 is metered at $3.00 per 1M input tokens and $15.00 per 1M output tokens: NativePort’s real, pass-through rate, no markup on individual calls. With a 1,048,576-token context window, a single large-context call can carry meaningfully more input tokens than smaller models; check usage.prompt_tokens on the response if you’re budgeting. Sign-up seeds $5 of credit automatically, and a zero balance fails every request with 402 rather than degrading output quality. Adding credit is the only place a fee applies: 5.5%, on top-ups of $10 to $5,000, never on the calls themselves. Full breakdown: pricing.
Where to go next
- Connect Kimi K3 to OpenCode and Pi: the same endpoint, wired into a terminal coding agent instead of a script.
- Can you run Kimi K3 locally?: what self-hosting actually requires, and why most people don’t.
- Hugging Face provider page: pricing range and scope across the whole router, not just this one model.
- Pricing: the full billing model.