Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
490 changes: 490 additions & 0 deletions providers/ollamacloud/language_model.go

Large diffs are not rendered by default.

144 changes: 144 additions & 0 deletions providers/ollamacloud/ollamacloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Package ollamacloud provides an implementation of the fantasy AI SDK for Ollama Cloud's language models.
package ollamacloud

import (
"bytes"
"cmp"
"context"
"encoding/json"
"fmt"
"io"
"maps"
"net/http"

"charm.land/fantasy"
)

const (
// Name is the name of the Ollama Cloud provider.
Name = "ollama-cloud"
// DefaultURL is the default URL for the Ollama Cloud API.
DefaultURL = "https://ollama.com"
)

type options struct {
baseURL string
apiKey string
name string
headers map[string]string
httpClient *http.Client
}

type provider struct {
options options
}

// Option defines a function that configures Ollama Cloud provider options.
type Option = func(*options)

// New creates a new Ollama Cloud provider with the given options.
func New(opts ...Option) (fantasy.Provider, error) {
providerOptions := options{
headers: map[string]string{},
httpClient: &http.Client{},
}
for _, o := range opts {
o(&providerOptions)
}

providerOptions.baseURL = cmp.Or(providerOptions.baseURL, DefaultURL)
providerOptions.name = cmp.Or(providerOptions.name, Name)

return &provider{options: providerOptions}, nil
}

// WithBaseURL sets the base URL for the Ollama Cloud provider.
func WithBaseURL(baseURL string) Option {
return func(o *options) {
o.baseURL = baseURL
}
}

// WithAPIKey sets the API key for the Ollama Cloud provider.
func WithAPIKey(apiKey string) Option {
return func(o *options) {
o.apiKey = apiKey
}
}

// WithName sets the name for the Ollama Cloud provider.
func WithName(name string) Option {
return func(o *options) {
o.name = name
}
}

// WithHeaders sets the headers for the Ollama Cloud provider.
func WithHeaders(headers map[string]string) Option {
return func(o *options) {
maps.Copy(o.headers, headers)
}
}

// WithHTTPClient sets the HTTP client for the Ollama Cloud provider.
func WithHTTPClient(client *http.Client) Option {
return func(o *options) {
o.httpClient = client
}
}

func (p *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.LanguageModel, error) {
return &languageModel{
provider: p,
modelID: modelID,
}, nil
}

func (p *provider) Name() string {
return p.options.name
}

// doRequest makes an HTTP request to the Ollama Cloud API.
func (p *provider) doRequest(ctx context.Context, reqBody any) (*http.Response, error) {
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, &fantasy.Error{
Title: "invalid argument",
Message: "failed to marshal request body",
Cause: err,
}
}

req, err := http.NewRequestWithContext(ctx, "POST", p.options.baseURL+"/api/chat", bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}

req.Header.Set("Content-Type", "application/json")
if p.options.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+p.options.apiKey)
}
for k, v := range p.options.headers {
req.Header.Set(k, v)
}

resp, err := p.options.httpClient.Do(req)
if err != nil {
return nil, err
}

if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, &fantasy.ProviderError{
Title: fmt.Sprintf("HTTP %d", resp.StatusCode),
Message: fmt.Sprintf("API error: %s", string(body)),
StatusCode: resp.StatusCode,
URL: p.options.baseURL + "/api/chat",
RequestBody: jsonBody,
ResponseBody: body,
}
}

return resp, nil
}
63 changes: 63 additions & 0 deletions providers/ollamacloud/provider_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package ollamacloud

import (
"encoding/json"

"charm.land/fantasy"
)

const (
// TypeProviderOptions is the registry type ID for Ollama Cloud options.
TypeProviderOptions = Name + ".options"
)

func init() {
fantasy.RegisterProviderType(TypeProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
var v ProviderOptions
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return &v, nil
})
}

// ProviderOptions represents additional options for the Ollama Cloud provider.
type ProviderOptions struct {
Think *bool `json:"think,omitempty"`
}

// Options implements the ProviderOptions interface.
func (*ProviderOptions) Options() {}

// MarshalJSON implements custom JSON marshaling with type info for ProviderOptions.
func (o ProviderOptions) MarshalJSON() ([]byte, error) {
type plain ProviderOptions
return fantasy.MarshalProviderType(TypeProviderOptions, plain(o))
}

// UnmarshalJSON implements custom JSON unmarshaling with type info for ProviderOptions.
func (o *ProviderOptions) UnmarshalJSON(data []byte) error {
type plain ProviderOptions
var p plain
if err := fantasy.UnmarshalProviderType(data, &p); err != nil {
return err
}
*o = ProviderOptions(p)
return nil
}

// NewProviderOptions creates new provider options for the Ollama Cloud provider.
func NewProviderOptions(opts *ProviderOptions) fantasy.ProviderOptions {
return fantasy.ProviderOptions{
Name: opts,
}
}

// ParseOptions parses provider options from a map for Ollama Cloud provider.
func ParseOptions(data map[string]any) (*ProviderOptions, error) {
var options ProviderOptions
if err := fantasy.ParseOptions(data, &options); err != nil {
return nil, err
}
return &options, nil
}
37 changes: 37 additions & 0 deletions providertests/ollamacloud_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package providertests

import (
"net/http"
"os"
"testing"

"charm.land/fantasy"
"charm.land/fantasy/providers/ollamacloud"
"charm.land/x/vcr"
)

var ollamacloudTestModels = []testModel{
{"gpt-oss-20b", "gpt-oss:20b-cloud", false},
{"gpt-oss-120b", "gpt-oss:120b-cloud", true},
}

func TestOllamaCloudCommon(t *testing.T) {
var pairs []builderPair
for _, m := range ollamacloudTestModels {
pairs = append(pairs, builderPair{m.name, ollamacloudBuilder(m.model), nil, nil})
}
testCommon(t, pairs)
}

func ollamacloudBuilder(model string) builderFunc {
return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) {
provider, err := ollamacloud.New(
ollamacloud.WithAPIKey(os.Getenv("FANTASY_OLLAMACLOUD_API_KEY")),
ollamacloud.WithHTTPClient(&http.Client{Transport: r}),
)
if err != nil {
return nil, err
}
return provider.LanguageModel(t.Context(), model)
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading