Skip to content
skyl

Provider

The seam between skyl and a model vendor. Four methods.

Provider is the interface everything in skyl exists to serve. It is deliberately four methods — small enough to implement in your own repository, easy to fake in tests, and easy to wrap.

Reference#

type Provider interface { Name() string Complete(ctx context.Context, req *Request) (*Response, error) Stream(ctx context.Context, req *Request) (Stream, error) Models(ctx context.Context) ([]ModelInfo, error) }

Parameters

  • Name — the adapter's short identifier, e.g. "anthropic". It appears in errors, responses and hook events.
  • Complete — runs a request to completion. Must honour ctx, populate Response.Raw, and set Provider and Model from the actual response.
  • Stream — runs a request delivering incremental events. The returned stream is bound to ctx; callers must close it.
  • Models — lists what the provider currently offers, queried live. Providers with no such endpoint return ErrUnsupported.

Caveats

  • Implementations must be safe for concurrent use by multiple goroutines.
  • An adapter in your own module is a first-class citizen: pass it to New and it inherits retry, hooks, validation and the gateway with no changes to skyl.
  • Everything cross-cutting lives in Client, not here. An adapter's whole job is translation.

The contract#

Beyond the signatures, an adapter is expected to:

RuleWhy
Honour ctxCancellation and deadlines must propagate, or Client cannot bound anything.
Always populate Response.RawIt is the caller's escape hatch.
Read Model from the responseProviders substitute; echoing hides it.
Honour ProviderOptionsAsserted by the shared contract suite, so the rule cannot be met by some adapters and quietly missed by others.
Return ErrUnsupported rather than dropping dataSilent data loss is the worst failure mode this library has.
Classify errors onto skyl sentinelsClient's retry loop branches on classification.
Bind the stream to ctx and never leak a goroutineA leaked goroutine per request is a 3am bug.
Report truncation rather than a clean endA stream cut short must not look complete.

Usage#

A fake for tests

goCompiles
type fakeProvider struct{ text string }

func (fakeProvider) Name() string { return "fake" }

func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
	return &skyl.Response{
		Provider:   "fake",
		Model:      "fake-model",
		Message:    skyl.Message{Role: skyl.RoleAssistant, Parts: []skyl.Part{skyl.Text{Text: p.text}}},
		StopReason: skyl.StopEndTurn,
		Usage:      skyl.Usage{InputTokens: 8, OutputTokens: 3},
		Raw:        json.RawMessage(`{}`),
	}, nil
}

func (fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
	return nil, skyl.ErrUnsupported
}

func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
	return nil, skyl.ErrUnsupported
}
type fakeProvider struct{ text string }

func (fakeProvider) Name() string { return "fake" }

func (p fakeProvider) Complete(context.Context, *skyl.Request) (*skyl.Response, error) {
	return &skyl.Response{
		Provider:   "fake",
		Model:      "fake-model",
		Message:    skyl.Message{Role: skyl.RoleAssistant, Parts: []skyl.Part{skyl.Text{Text: p.text}}},
		StopReason: skyl.StopEndTurn,
		Usage:      skyl.Usage{InputTokens: 8, OutputTokens: 3},
		Raw:        json.RawMessage(`{}`),
	}, nil
}

func (fakeProvider) Stream(context.Context, *skyl.Request) (skyl.Stream, error) {
	return nil, skyl.ErrUnsupported
}

func (fakeProvider) Models(context.Context) ([]skyl.ModelInfo, error) {
	return nil, skyl.ErrUnsupported
}

Wrap it in a real skyl.New(...) so your tests exercise the actual validation and retry code rather than a mock of it.

Decorating an adapter

// Embedding gives you Name, Stream and Models unchanged.
type logging struct{ skyl.Provider }

func (l logging) Complete(ctx context.Context, req *skyl.Request) (*skyl.Response, error) {
	start := time.Now()
	resp, err := l.Provider.Complete(ctx, req)
	log.Printf("%s took %s", l.Name(), time.Since(start))
	return resp, err
}

client := skyl.New(logging{Provider: openai.New(key)})
// Embedding gives you Name, Stream and Models unchanged.
type logging struct{ skyl.Provider }

func (l logging) Complete(ctx context.Context, req *skyl.Request) (*skyl.Response, error) {
	start := time.Now()
	resp, err := l.Provider.Complete(ctx, req)
	log.Printf("%s took %s", l.Name(), time.Since(start))
	return resp, err
}

client := skyl.New(logging{Provider: openai.New(key)})

Selecting one at runtime

goCompiles
func pick(name string) (skyl.Provider, error) {
	switch name {
	case "anthropic":
		return anthropic.New(os.Getenv("ANTHROPIC_API_KEY")), nil
	case "openai":
		return openai.New(os.Getenv("OPENAI_API_KEY")), nil
	default:
		return nil, fmt.Errorf("unknown provider %q", name)
	}
}
func pick(name string) (skyl.Provider, error) {
	switch name {
	case "anthropic":
		return anthropic.New(os.Getenv("ANTHROPIC_API_KEY")), nil
	case "openai":
		return openai.New(os.Getenv("OPENAI_API_KEY")), nil
	default:
		return nil, fmt.Errorf("unknown provider %q", name)
	}
}

Troubleshooting#

Why four methods and not two, or ten?

Two would mean folding streaming into Complete with a flag, which makes the return type dishonest. Ten would mean modelling embeddings, moderation and image generation — each a genuinely different shape, several of which not every vendor offers. An interface half its implementations return ErrUnsupported from is not an interface.

Four is the set every text-model vendor actually implements. Recorded as ADR-0002.

My adapter's Models has no endpoint to call

Return ErrUnsupported, not an empty slice and not a hardcoded list. Client recognises it and does not retry, because a provider that cannot list models never will.

Edit this page on GitHub