September 25, 2026
GMI Cloud MaaS serves openai/gpt-6-astra and moonshotai/kimi-k3 through one base URL, key, and invoice. October 2026 list input/output rates are $10/$50 and $3/$15 per million tokens respectively. Compare them with task-specific tests rather than treating the price gap as a quality verdict. (Model-as-a-Service platform)
Start from the catalog parameters:
openai/gpt-6-astra | moonshotai/kimi-k3 | |
|---|---|---|
| Status in the GMI Cloud model catalog | ACTIVE, available on MaaS | ACTIVE, available on MaaS |
| Input / output per 1M tokens | $10.00 / $50.00 | $3.00 / $15.00 |
| Second price tier | from 272,000 input tokens: $20.00 / $75.00 | none listed |
| Cache read per 1M tokens | $1.00 standard tier, $2.00 second tier | $0.30 |
| Cache write per 1M tokens | $12.50 standard tier, $25.00 second tier | no cache write price listed |
| Listed discount on October 15, 2026 | none | none |
| Context window | 1,050,000 | 1,048,576 |
| Quantization listed | fp8 | fp8 |
| Capability detail in the catalog entry | text and image input; chat completions and Responses examples including function calling and file input | explicit flags for tool calling, image input, and document input; chat completions examples including function calling |
| Provider | OpenAI | Moonshot AI |
Prices and parameters were read on October 15, 2026. Recheck the model library before budgeting; GMI's Astra engineering briefing also cites $10 input and $50 output per million tokens. (console.gmicloud.ai/model-library; wrote about scaling Astra traffic)
Two numbers in that table decide most of what follows. The standard-tier price ratio is 3.33x on both input and output. From the 272,000 token threshold up, it widens to 6.67x on input and 5x on output. A test that does not separate those two regimes will produce an average that describes neither.
Testing both models under one GMI Cloud account gives you one base URL and key, one invoice, one usage view, and a reversible decision at the end. GMI Cloud is an AI-native inference cloud, and its Model-as-a-Service layer provides access to "the world's most widely used closed-source models and the most popular open-source models, all through one API." For a two-model comparison those four things are concrete:
https://api.gmi-serving.com/v1/chat/completions. The developer page puts it as "Keep your code, change the model ID," and GET /v1/models lists everything your key can call.What it does not change is the test design. One key does not make two models comparable: the price curves differ, the context ceilings differ, and each model reports its own token counts, so cost has to be read per model from each response rather than assumed from prompt length.
One endpoint, one key, and a model string that changes.
curl https://api.gmi-serving.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $GMI_API_KEY" \
-d '{
"model": "openai/gpt-6-astra",
"messages": [{"role": "user", "content": "Summarize this changelog."}],
"temperature": 0,
"max_completion_tokens": 1024,
"context_length_exceeded_behavior": "error"
}'
Swap "openai/gpt-6-astra" for "moonshotai/kimi-k3" and send the output cap as max_tokens, which is the field that entry's own examples use, and the call is otherwise identical. Both catalog entries document the same OpenAI-compatible chat completions interface, so the harness does not need a per-model adapter. The Astra entry additionally documents Responses API examples; during a bake-off, pick one transport and use it for both models, because a comparison that mixes /v1/responses with /v1/chat/completions is comparing two request paths as well as two models.
Seven settings decide whether the two columns are comparable: the sampling parameters and the output cap, truncation behavior, task buckets, a separate column for long context, one API key per bucket, usage capture on streamed calls, and the schedule. They are listed below in the order they bite.
1. Pin every sampling parameter explicitly, and check which field caps output. The console parameter panels are not identical across the two entries: the Kimi K3 entry carries a Max Tokens default of 4,096, and relying on per-model defaults silently changes output length, latency, and cost at the same time. There is a second trap in the same place. The Astra entry's API examples cap generation with max_completion_tokens, while the Kimi K3 examples use max_tokens. Send one short call per model before the run and confirm the cap actually applied, otherwise the two models answer at different lengths and the latency and cost columns stop being comparable.
2. Turn off silent truncation. The LLM API reference documents context_length_exceeded_behavior, which defaults to truncate and accepts error. During evaluation, set it to error. The two listed windows differ by 1,424 tokens, and each window has to hold the prompt plus whatever generation budget you request, so near the ceiling the two models stop accepting the same request at slightly different points. With the default, the model that still accepts it answers confidently from a quietly shortened prompt, and the scoring sheet records that as a quality difference.
3. Bucket by real tasks and assign a primary repeatable metric: test pass rate for coding, schema-valid calls for agents, or labeled field accuracy for extraction; log latency alongside each. GMI's August benchmark roundup offers a taxonomy, while the Gemini 3.8 Flash versus Astra extraction article provides a single-bucket example. Public benchmark scores cannot replace your own tasks. (August 2026 benchmark roundup; Gemini 3.8 Flash vs GPT-6 Astra for document extraction)
4. Put the long-context bucket in its own column. Astra's second tier applies from 272,000 input tokens up. Mixing those calls into a blended average hides the single largest cost difference in the test. The budget table below shows how badly.
5. Issue a separate API key per bucket. Console's usage view filters by API key, so one key per bucket turns post-run billing reconciliation into a lookup rather than a spreadsheet exercise. This matters more than it sounds: the chat completions usage object returns prompt_tokens, completion_tokens, and total_tokens, and there is no documented field for cache hits, so any claim about caching savings has to be settled against Console usage and billing rather than against the response body.
6. Capture usage on streamed calls from the final chunk. The API reference states that "Streaming responses include usage stats in final chunk." A harness that stops reading at the last content delta loses the token counts for every streamed call. The harness below sends stream: False for that reason, since a bake-off needs the token counts more than it needs first-token latency; if you turn streaming on to measure time to first token, read usage from the final chunk and not from the last delta.
7. Schedule the run. LLM rate limits on GMI Cloud are measured in tokens per minute and, per the rate limit documentation, "These limits are enforced at the organization level." Enforcement at that level means a separate evaluation key is not a separate quota, so put the run in a window where organization throughput is free, or ask about a tier change first. Pace the long-context bucket in particular: a single 300,000 token prompt spends 300,000 tokens of that minute by itself, so run those calls in sequence rather than in parallel and leave headroom for the retries.
Write every outcome, including the failures, as a row in an append-only JSONL file, retry only timeouts, transport errors, 429 and 5xx, and carry the usage block forward from every attempt that reported one. An evaluation script that crashes on the first 429 produces a partial matrix and an unusable cost total instead. The harness below records failures as data, keeps the usage block from any response that was billed, and refuses to retry client errors that are really test bugs.
import json
import os
import random
import time
from email.utils import parsedate_to_datetime
import requests
BASE_URL = "https://api.gmi-serving.com/v1"
API_KEY = os.environ["GMI_API_KEY"]
MODELS = ["openai/gpt-6-astra", "moonshotai/kimi-k3"]
# USD per 1M tokens, list price from the GMI Cloud model library, October 15, 2026.
PRICING = {
"openai/gpt-6-astra": [
{"from_input_tokens": 0, "input": 10.00, "output": 50.00},
{"from_input_tokens": 272_000, "input": 20.00, "output": 75.00},
],
"moonshotai/kimi-k3": [
{"from_input_tokens": 0, "input": 3.00, "output": 15.00},
],
}
SAMPLING = {"temperature": 0.0, "top_p": 1.0}
OUTPUT_CAP = 1024
# Each catalog entry's examples cap generation with a different field name. Send the one
# the entry documents, then confirm from the first response that the cap actually applied.
OUTPUT_CAP_FIELD = {
"openai/gpt-6-astra": "max_completion_tokens",
"moonshotai/kimi-k3": "max_tokens",
}
MAX_PROMPT_CHARS = 3_000_000
MAX_RETRY_WAIT = 60.0
class PreflightError(Exception):
"""Raised before anything billable happens."""
def list_price(model, prompt_tokens, completion_tokens):
tier = PRICING[model][0]
for candidate in PRICING[model]:
if prompt_tokens >= candidate["from_input_tokens"]:
tier = candidate
return (prompt_tokens * tier["input"] + completion_tokens * tier["output"]) / 1_000_000
def retry_delay(response, attempt):
"""Retry-After may be seconds or an HTTP date, and may be missing or junk."""
header = response.headers.get("Retry-After")
backoff = float(min(2 ** attempt, 30))
if not header:
return backoff
try:
wait = float(header)
except (TypeError, ValueError):
try:
wait = parsedate_to_datetime(header).timestamp() - time.time()
except (TypeError, ValueError, OverflowError):
return backoff
return min(max(0.0, wait), MAX_RETRY_WAIT)
def run_one(model, messages, timeout=120, max_attempts=4, sleep=time.sleep):
if not messages or not any(str(m.get("content", "")).strip() for m in messages):
raise PreflightError("empty prompt")
if sum(len(str(m.get("content", ""))) for m in messages) > MAX_PROMPT_CHARS:
raise PreflightError("prompt above local guard")
payload = {
"model": model,
"messages": messages,
"stream": False,
"context_length_exceeded_behavior": "error",
OUTPUT_CAP_FIELD.get(model, "max_tokens"): OUTPUT_CAP,
**SAMPLING,
}
headers = {"Authorization": "Bearer " + API_KEY, "Content-Type": "application/json"}
billed = {"prompt_tokens": 0, "completion_tokens": 0, "cost": 0.0,
"seen": False, "gap": False}
def account(body, status_code):
# Every attempt that reports usage consumed tokens, including failed ones.
usage = body.get("usage") if isinstance(body, dict) else None
if not isinstance(usage, dict):
# A 4xx, including 429, returns no completion, so there is nothing to account
# for. Anything else that answered without usage leaves a real gap.
if status_code < 400 or status_code >= 500:
billed["gap"] = True
return
prompt_tokens = usage.get("prompt_tokens") or 0
completion_tokens = usage.get("completion_tokens") or 0
billed["prompt_tokens"] += prompt_tokens
billed["completion_tokens"] += completion_tokens
billed["cost"] += list_price(model, prompt_tokens, completion_tokens)
billed["seen"] = True
def row(status, attempt, **extra):
if not billed["seen"]:
usage_state = "missing"
elif billed["gap"]:
usage_state = "partial" # at least one attempt reported nothing
else:
usage_state = "complete"
result = {
"model": model,
"status": status,
"attempts": attempt,
"prompt_tokens": billed["prompt_tokens"],
"completion_tokens": billed["completion_tokens"],
"usage": usage_state,
"list_cost_usd": round(billed["cost"], 6) if billed["seen"] else None,
}
result.update(extra)
return result
for attempt in range(1, max_attempts + 1):
started = time.perf_counter()
try:
response = requests.post(
BASE_URL + "/chat/completions", json=payload, headers=headers, timeout=timeout
)
except requests.Timeout:
if attempt == max_attempts:
return row("timeout", attempt)
sleep(min(2 ** attempt, 30) + random.random())
continue
except requests.RequestException as exc:
if attempt == max_attempts:
return row("transport_error", attempt, error=str(exc)[:200])
sleep(min(2 ** attempt, 30) + random.random())
continue
latency = time.perf_counter() - started
try:
body = response.json()
except ValueError:
body = None
account(body, response.status_code)
if response.status_code == 429 or response.status_code >= 500:
if attempt == max_attempts:
return row("http_%d" % response.status_code, attempt)
sleep(min(retry_delay(response, attempt) + random.random(), MAX_RETRY_WAIT))
continue
if response.status_code >= 400:
# 4xx other than 429 is a bug in the request, not flakiness. Do not retry it.
return row("http_%d" % response.status_code, attempt, error=response.text[:200])
if not isinstance(body, dict):
return row("unexpected_body", attempt, error=(response.text or "")[:200])
choices = body.get("choices") or []
first = choices[0] if choices and isinstance(choices[0], dict) else {}
message = first.get("message") or {}
text = message.get("content") or ""
tool_calls = message.get("tool_calls") or []
if text.strip():
status = "ok"
elif tool_calls:
status = "tool_calls" # a tool-call turn is a real answer, not an empty one
else:
status = "empty_completion"
return row(
status,
attempt,
latency_s=round(latency, 3),
finish_reason=first.get("finish_reason"),
output=text,
tool_calls=tool_calls,
)
return row("exhausted", max_attempts)
def run_suite(tasks, out_path="bakeoff.jsonl"):
rows = []
with open(out_path, "a", encoding="utf-8") as handle:
for task in tasks:
for model in MODELS:
row = {"task_id": task["id"], "bucket": task["bucket"], "model": model}
try:
row.update(run_one(model, task["messages"]))
except PreflightError as exc:
row.update({"status": "preflight_error", "error": str(exc)})
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
rows.append(row)
return rows
Seven behaviors in that code are worth keeping even if you rewrite the rest.
requests.RequestException retry with backoff and then return a status, so one dropped connection does not end a six-hour run.Retry-After is parsed defensively. The header is allowed to be either a delay in seconds or an HTTP date, and a junk value should not crash the client, so the parser handles all three and clamps the wait.context_length_exceeded error raised under rule 2 is a finding rather than a retry loop.usage field of complete, partial, or missing. A response with no usage block leaves the cost null rather than $0, and a run where an attempt that answered reported no usage comes back as partial, so a subtotal is never mistaken for a verified total. A 429 that is retried into a clean success stays complete, because a rejected request returned no completion to account for.max_completion_tokens for the Astra ID and max_tokens for the Kimi K3 ID, which is what rule 1 asks for, so one harness does not quietly leave one of the two models uncapped.content: null, populated tool_calls, and finish_reason: tool_calls is recorded as tool_calls with the arguments preserved, which is exactly the case the tool-use bucket is measuring.Cost is computed through the tier lookup rather than a flat rate, which is the only way the long-context bucket prices correctly.
A 220-call matrix per model lists at $175.40 in total, $149.00 for GPT-6 Astra and $26.40 for Kimi K3. That figure comes from a modest but realistic shape: 200 standard tasks at roughly 8,000 input and 1,000 output tokens, plus 20 long-context tasks at roughly 300,000 input and 2,000 output tokens, run against both models.
| Bucket | Calls per model | Tokens per call | Astra at list price | Kimi K3 at list price |
|---|---|---|---|---|
| Standard | 200 | 8,000 in / 1,000 out | $26.00 | $7.80 |
| Long context | 20 | 300,000 in / 2,000 out | $123.00 | $18.60 |
| Total | 220 | $149.00 | $26.40 |
At $175.40, the whole run costs under 4% of the $4,550 monthly gap that a single 50,000-call bucket carries between these two models, priced in the decision rules below. The useful finding is in the distribution: the long-context bucket is 9.1% of the calls and 82.6% of the Astra bill. Per call, the gap is $0.091 in the standard bucket and $5.22 in the long-context bucket.
At equal cache-hit rates, both models' one-tenth-priced cache reads preserve the price ranking: Astra $1 versus $10 fresh input, Kimi $0.30 versus $3. Equal hit rates are an assumption; verify billing because the API does not report hits. Astra also lists $12.50/1M cache writes, above $10 fresh input, so a never-reused write can cost more. Kimi lists no write price. Model reuse economics separately.
GPT-6 Astra earns its 3.33x token price on any bucket where the quality difference you can measure is worth more than the monthly dollar gap that bucket generates, which on standard-sized tasks reaches $4,550 at 50,000 calls a month. Convert ratios into monthly dollars per bucket, then apply three rules.
Rule 1: price the gap at production volume. At 8,000 in and 1,000 out, GMI Cloud MaaS list pricing puts Astra at $0.13 per call and Kimi K3 at $0.039. A bucket running 50,000 calls a month carries a $4,550 monthly gap on the same invoice. That number, not the 3.33x ratio, is what a quality difference has to be worth.
Rule 2: a win inside your scoring noise is not a win. Measure that noise before you read the scoreboard: rescore one bucket's outputs a second time, or score a model against itself across two runs at the same sampling settings, and take the spread you get back as your noise floor. Any gap between the two models smaller than that floor goes to the cheaper model ID. Keep per-bucket sample counts high enough that the floor is itself stable, which is exactly why rule 3 of the test design exists.
Rule 3: attack the 272,000 token threshold before you attack the price. A 300,000 input and 2,000 output token call on Astra lists at $6.15 under the second tier. Trim the same task to 120,000 input tokens with retrieval, keeping the 2,000 output tokens, and it lists at $1.30 under the standard tier, a 4.7x reduction with no model change and no negotiation. Measure how many of your long-context tasks genuinely need the full window before budgeting for the second tier.
The common outcome is a split: the cheaper model ID on high-volume buckets, the pricier one on the handful of buckets where it pays for itself. That split is cheap to operate on GMI Cloud precisely because both IDs live on one key. The runtime side of operating a split, routing rules and fallback behavior once the bake-off is over, is covered in switching between GPT-6 Astra, Gemini 3.8 Flash, and DeepSeek V4.1 Flash. If selecting per request becomes its own project, GMI Cloud's GMI Router automates the choice across the models your workspace allows.
Both models in this comparison are served through the MaaS API, so the account-level answer stays the same whichever one wins: one key, one endpoint, one invoice. The other deployment track on the same account covers a different kind of workload, a model you own. GMI Cloud's Prime Inference takes "Any open-source, fine-tuned, or proprietary weights" and runs them on reserved single-tenant NVIDIA GPUs billed per GPU-hour, with the weights pre-loaded so calls land warm. If your roadmap adds a model of your own after this bake-off, that work does not need a new vendor, and the MaaS documentation notes that teams "transition between these deployment models without changing APIs."
Can one API key call both GPT-6 Astra and Kimi K3? Yes. Both use https://api.gmi-serving.com/v1 with a single GMI key. Check availability with GET /v1/models, then change the request's model field.
How much should a two-model evaluation cost? The illustrated 220 calls per model—200 at 8K input/1K output and 20 at 300K input/2K output—cost $149.00 for Astra and $26.40 for Kimi, or $175.40 total at October 2026 list rates. Long-context calls make up 82.6% of the Astra bill, so budget by bucket.
Do the two models have the same context window? Astra lists 1,050,000 tokens and Kimi 1,048,576, a 1,424-token difference. Account for prompt plus requested generation. Set context_length_exceeded_behavior to error during testing to avoid the default silent truncation.
Does caching change which model is cheaper? Not at equal hit rates: cached-input rates are one-tenth of fresh input for both models. Actual rates differ by workload, and usage exposes only prompt_tokens, completion_tokens, and total_tokens. Reconcile Console billing rather than inferring hits from responses.
Can the bake-off and production traffic run at the same time? Yes, but keys share the organization's TPM quota. Use a quiet window or arrange the needed tier before the evaluation.
What happens after a model wins? Keep the winning model ID on MaaS and retain the other for suitable buckets. Add GMI Router for automated selection within the approved pool, or Prime Inference for customer-owned weights. The cost-per-million-tokens article covers dedicated-versus-token economics. (cost per million tokens)
Create a key in the GMI Cloud console, copy the two model IDs, and point the harness above at them. If you want the test matrix, rate limit tier, and pricing reviewed against your traffic profile first, our team will walk through it with you. Start from MaaS pricing and coverage or the developer quickstart.
Colin Mo
GMI Cloud helps you architect, deploy, optimize, and scale your AI strategies
