You asked for gpt-5.6. Something answered. Response.Model tells you what,
and it is not always the same string — which matters more than it sounds when
you are comparing cost, latency or quality across a fleet.
You will learn
- Why
Response.Modelis read from the response - The three reasons a provider serves a different model
- How to detect and record substitutions
- What each adapter reads it from
It is not an echo#
resp, err := client.Complete(ctx, &skyl.Request{Model: "gpt-5.6", /* … */})
fmt.Println(req.Model) // gpt-5.6 — what you asked for
fmt.Println(resp.Model) // gpt-5.6-2026-07-09 — what answeredresp, err := client.Complete(ctx, &skyl.Request{Model: "gpt-5.6", /* … */})
fmt.Println(req.Model) // gpt-5.6 — what you asked for
fmt.Println(resp.Model) // gpt-5.6-2026-07-09 — what answeredEvery adapter reads it from the provider's reply: Anthropic and the
OpenAI-format adapters from the response body's model field, Gemini from
modelVersion.
Why they differ#
Alias resolution. gpt-5.6 is a moving pointer to a dated snapshot.
Providers resolve it at request time and tell you which one they picked.
Capacity fallback. Under load some providers serve a nearby model rather than queueing or failing.
Account routing. Enterprise deployments may pin a specific snapshot for reproducibility, so the alias resolves differently than it does on a personal key.
Deep diveWhy this is worth a field of its own
Because every number you compute about a model is wrong if you attribute it to the wrong one.
A latency regression that appears when a provider silently moves an alias to a
new snapshot looks like your regression. A cost report grouped by
req.Model merges two snapshots with different pricing into one line. A quality
evaluation that says "gpt-5.6 scores 0.82" is meaningless if half the run was
served by a different snapshot.
skyl could have echoed the request — it would have been simpler, and nobody would have noticed. Reading the response is the difference between a number and a guess.
Recording it#
The right thing to group metrics by is resp.Model, and the right thing to log
alongside it is resp.ID:
skyl.WithHook(func(_ context.Context, ev skyl.HookEvent) {
// ev.Model is what was asked for; ev.ResponseModel is what answered.
metrics.Record(ev.Provider, ev.ResponseModel, ev.Duration, ev.Usage.TotalTokens())
if ev.ResponseModel != "" && ev.ResponseModel != ev.Model {
log.Printf("substitution: asked %q, served %q (response %s)",
ev.Model, ev.ResponseModel, ev.ResponseID)
}
})skyl.WithHook(func(_ context.Context, ev skyl.HookEvent) {
// ev.Model is what was asked for; ev.ResponseModel is what answered.
metrics.Record(ev.Provider, ev.ResponseModel, ev.Duration, ev.Usage.TotalTokens())
if ev.ResponseModel != "" && ev.ResponseModel != ev.Model {
log.Printf("substitution: asked %q, served %q (response %s)",
ev.Model, ev.ResponseModel, ev.ResponseID)
}
})HookEvent carries both deliberately, for exactly this comparison.
When it is empty#
Response.Model is best-effort. A provider that does not report it leaves the
field empty — so guard before comparing, or every response from such a provider
looks like a substitution.
Reproducibility#
If you need a fixed model for an evaluation or a regression test, pin the dated snapshot in your configuration rather than the alias:
FastModel: "gpt-5.6-2026-07-09" // pinned, reproducible
SmartModel: "claude-opus-5" // moving, always currentFastModel: "gpt-5.6-2026-07-09" // pinned, reproducible
SmartModel: "claude-opus-5" // moving, always currentskyl passes both through untouched, so this is entirely your choice — which is the point of pass-through model IDs.
Recap
Response.Modelis read from the provider's reply, not echoed from your request.- Aliases, capacity fallback and account routing all cause substitutions.
- Group metrics by
resp.Model/ev.ResponseModel, or your numbers merge snapshots. - Substitution is normal — log it at info, and only flag a different model family.
- The field is best-effort; guard against empty before comparing.
- Pin a dated snapshot in config when you need reproducibility.
Try out some challenges
Each of these is solvable with what this page covered. Run them against the sandbox — no API key needed.
Detect an alias moving under you
Write a check that alerts when the snapshot behind an alias changes, without alerting on every request.
Show hint
Remember the last value you saw per requested model.
Show solution
type tracker struct {
mu sync.Mutex
seen map[string]string // requested model -> last snapshot that answered
}
func (t *tracker) observe(asked, served string) {
if served == "" || served == asked {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if prev, ok := t.seen[asked]; ok && prev != served {
log.Printf("alias %q moved: %s -> %s", asked, prev, served)
}
t.seen[asked] = served
}type tracker struct {
mu sync.Mutex
seen map[string]string // requested model -> last snapshot that answered
}
func (t *tracker) observe(asked, served string) {
if served == "" || served == asked {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if prev, ok := t.seen[asked]; ok && prev != served {
log.Printf("alias %q moved: %s -> %s", asked, prev, served)
}
t.seen[asked] = served
}This fires once per move rather than once per request, which is the difference between a useful signal and noise. Wire it into a hook and you will know the morning a provider re-points an alias — usually before they announce it.