Skip to content
skyl

Model IDs Are Just Strings

The most consequential decision in the project, and the price it charges you.

skyl ships no model-name constants and never validates a model against a list. Request.Model is an opaque string passed straight to the provider. This page is about why, and about the trade you are accepting.

You will learn

  • Why a curated model list is guaranteed to be wrong
  • What you give up by not having one
  • How to discover what a provider currently offers
  • What model metadata skyl does and does not give you

The decision#

Model: "claude-opus-5"     // works the day it launches
Model: "gpt-5.6"
Model: "gemini-3.6-flash"
Model: "llama3.3"          // your own Ollama tag
Model: "claude-opus-5"     // works the day it launches
Model: "gpt-5.6"
Model: "gemini-3.6-flash"
Model: "llama3.3"          // your own Ollama tag

No enum, no validation table, no release required.

Deep diveWhy a curated list is guaranteed to be wrong

Consider a four-week window in mid-2026:

| Model | Released | |---|---| | GPT-5.6 Sol | 9 Jul 2026 | | Gemini 3.6 Flash | 21 Jul 2026 | | Claude Opus 5 | 24 Jul 2026 | | Qwen3.7 Flash | 27 Jul 2026 |

Any hardcoded table is wrong within weeks. And the failure mode is the worst possible one for a library: skyl would be rejecting a model you are entitled to use and are already paying for, because skyl has not cut a release.

Pass-through is correct forever. Recorded as ADR-0004.

The price#

Handle it explicitly, because the message is otherwise easy to misread as a credential problem:

goCompiles
resp, err := client.Complete(ctx, req)
if errors.Is(err, skyl.ErrNotFound) {
	return fmt.Errorf("no model %q on %s — check the spelling, or your account's access", req.Model, resp.Provider)
}
resp, err := client.Complete(ctx, req)
if errors.Is(err, skyl.ErrNotFound) {
	return fmt.Errorf("no model %q on %s — check the spelling, or your account's access", req.Model, resp.Provider)
}

Discovering what is available#

Ask the provider, live:

goCompiles
models, err := client.Models(ctx)
if err != nil {
	return err
}
for _, m := range models {
	fmt.Printf("%s (%s) context=%d\n", m.ID, m.DisplayName, m.ContextWindow)
}
models, err := client.Models(ctx)
if err != nil {
	return err
}
for _, m := range models {
	fmt.Printf("%s (%s) context=%d\n", m.ID, m.DisplayName, m.ContextWindow)
}

That hits the provider's real models endpoint, so the answer is never stale — it is not skyl's opinion, it is the provider's answer.

All four adapters support it; none return ErrUnsupported. What they report varies:

Fieldanthropicopenaiopenaicompatgemini
ID, Provider, Rawyesyesyesyes
DisplayNameyesemptyhost-dependentyes
ContextWindowyesemptyhost-dependentyes
MaxOutputTokensyesnever setnever setyes

OpenAI's models endpoint simply has no such fields — the gap is upstream, not in the adapter. ModelInfo.Raw carries the provider's untouched entry, so anything they do report is reachable.

Model metadata#

Context window, pricing and modality are genuinely useful, and skyl does not have them for every provider today. The plan is a generated registry — a scheduled CI job that queries live provider endpoints and regenerates a committed file — rather than a hand-typed table that silently rots. Until it lands, ModelInfo carries what the provider returns.

Practical advice#

Put model IDs in configuration, not in code. Since skyl will not validate them, your config layer is the right place to fail fast:

goCompiles
type Config struct {
	FastModel  string `env:"FAST_MODEL,required"`
	SmartModel string `env:"SMART_MODEL,required"`
}
type Config struct {
	FastModel  string `env:"FAST_MODEL,required"`
	SmartModel string `env:"SMART_MODEL,required"`
}

That way changing model is a deploy, not a release — which is the whole benefit pass-through was protecting.

Recap

  • Model IDs are opaque strings; skyl ships no constants and validates nothing.
  • A curated list would eventually reject a model you are entitled to use.
  • The price is that a typo costs a round trip and arrives as ErrNotFound.
  • Client.Models(ctx) asks the provider live, so the answer is never stale.
  • What model listing reports varies sharply; OpenAI supplies ID and little else.
  • Keep model IDs in configuration so changing one is a deploy, not a release.

Try out some challenges

Each of these is solvable with what this page covered. Run them against the sandbox — no API key needed.

Validate model IDs at startup

You cannot get compile-time checking, but you can fail at boot rather than on the first user request.

Show hint

Client.Models gives you the live list. Check your configured IDs against it once, during startup.

Show solution
goCompiles
func checkModels(ctx context.Context, c *skyl.Client, want ...string) error {
	models, err := c.Models(ctx)
	if err != nil {
		// A provider that cannot list models is not a reason to refuse to
		// start — pass-through still works. Warn and continue.
		log.Printf("could not verify model IDs: %v", err)
		return nil
	}

	have := make(map[string]bool, len(models))
	for _, m := range models {
		have[m.ID] = true
	}
	for _, w := range want {
		if !have[w] {
			return fmt.Errorf("configured model %q is not offered by this provider", w)
		}
	}
	return nil
}
func checkModels(ctx context.Context, c *skyl.Client, want ...string) error {
	models, err := c.Models(ctx)
	if err != nil {
		// A provider that cannot list models is not a reason to refuse to
		// start — pass-through still works. Warn and continue.
		log.Printf("could not verify model IDs: %v", err)
		return nil
	}

	have := make(map[string]bool, len(models))
	for _, m := range models {
		have[m.ID] = true
	}
	for _, w := range want {
		if !have[w] {
			return fmt.Errorf("configured model %q is not offered by this provider", w)
		}
	}
	return nil
}

Note the deliberate non-failure when listing itself fails. Refusing to start because a metadata endpoint was down would be worse than the typo you are guarding against.

Edit this page on GitHub