diff --git a/providers/atlascloud/atlascloud.go b/providers/atlascloud/atlascloud.go new file mode 100644 index 000000000..f56ccf3e6 --- /dev/null +++ b/providers/atlascloud/atlascloud.go @@ -0,0 +1,100 @@ +// Package atlascloud provides an implementation of the fantasy AI SDK for Atlas Cloud's OpenAI-compatible language models. +package atlascloud + +import ( + "charm.land/fantasy" + "charm.land/fantasy/providers/openaicompat" + "github.com/charmbracelet/openai-go/option" +) + +const ( + // DefaultURL is the default URL for the Atlas Cloud LLM API. + DefaultURL = "https://api.atlascloud.ai/v1" + // Name is the name of the Atlas Cloud provider. + Name = "atlascloud" +) + +type options struct { + openaicompatOptions []openaicompat.Option +} + +// Option defines a function that configures Atlas Cloud provider options. +type Option = func(*options) + +// New creates a new Atlas Cloud provider with the given options. +func New(opts ...Option) (fantasy.Provider, error) { + providerOptions := options{ + openaicompatOptions: []openaicompat.Option{ + openaicompat.WithName(Name), + openaicompat.WithBaseURL(DefaultURL), + }, + } + for _, o := range opts { + o(&providerOptions) + } + return openaicompat.New(providerOptions.openaicompatOptions...) +} + +// WithAPIKey sets the API key for the Atlas Cloud provider. +func WithAPIKey(apiKey string) Option { + return func(o *options) { + o.openaicompatOptions = append(o.openaicompatOptions, openaicompat.WithAPIKey(apiKey)) + } +} + +// WithBaseURL sets the base URL for the Atlas Cloud provider. +func WithBaseURL(url string) Option { + return func(o *options) { + o.openaicompatOptions = append(o.openaicompatOptions, openaicompat.WithBaseURL(url)) + } +} + +// WithName sets the name for the Atlas Cloud provider. +func WithName(name string) Option { + return func(o *options) { + o.openaicompatOptions = append(o.openaicompatOptions, openaicompat.WithName(name)) + } +} + +// WithHeaders sets the headers for the Atlas Cloud provider. +func WithHeaders(headers map[string]string) Option { + return func(o *options) { + o.openaicompatOptions = append(o.openaicompatOptions, openaicompat.WithHeaders(headers)) + } +} + +// WithHTTPClient sets the HTTP client for the Atlas Cloud provider. +func WithHTTPClient(client option.HTTPClient) Option { + return func(o *options) { + o.openaicompatOptions = append(o.openaicompatOptions, openaicompat.WithHTTPClient(client)) + } +} + +// WithUserAgent sets an explicit User-Agent header, overriding the default and any +// value set via WithHeaders. +func WithUserAgent(ua string) Option { + return func(o *options) { + o.openaicompatOptions = append(o.openaicompatOptions, openaicompat.WithUserAgent(ua)) + } +} + +// WithObjectMode sets the object generation mode for the Atlas Cloud provider. +func WithObjectMode(om fantasy.ObjectMode) Option { + return func(o *options) { + o.openaicompatOptions = append(o.openaicompatOptions, openaicompat.WithObjectMode(om)) + } +} + +// WithUseResponsesAPI configures the provider to use the responses API for models that support it. +func WithUseResponsesAPI() Option { + return func(o *options) { + o.openaicompatOptions = append(o.openaicompatOptions, openaicompat.WithUseResponsesAPI()) + } +} + +// WithResponsesAPIFunc sets a custom filter for which models use the Responses API. +func WithResponsesAPIFunc(fn func(modelID string) bool) Option { + return func(o *options) { + o.openaicompatOptions = append(o.openaicompatOptions, openaicompat.WithResponsesAPIFunc(fn)) + } +} diff --git a/providers/atlascloud/atlascloud_test.go b/providers/atlascloud/atlascloud_test.go new file mode 100644 index 000000000..ed5fb0b83 --- /dev/null +++ b/providers/atlascloud/atlascloud_test.go @@ -0,0 +1,85 @@ +package atlascloud + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "charm.land/fantasy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProviderName(t *testing.T) { + t.Parallel() + + provider, err := New(WithAPIKey("k")) + require.NoError(t, err) + model, err := provider.LanguageModel(t.Context(), "qwen/qwen3.5-flash") + require.NoError(t, err) + + assert.Equal(t, Name, model.Provider()) +} + +func TestDefaultRequest(t *testing.T) { + t.Parallel() + + var captured []map[string]string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := make(map[string]string) + for k, v := range r.Header { + if len(v) > 0 { + h[k] = v[0] + } + } + captured = append(captured, h) + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1711115037, + "model": "qwen/qwen3.5-flash", + "choices": []map[string]any{ + { + "index": 0, + "message": map[string]any{ + "role": "assistant", + "content": "Hi there", + }, + "finish_reason": "stop", + }, + }, + "usage": map[string]any{ + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + }, + }) + })) + defer server.Close() + + provider, err := New(WithAPIKey("k"), WithBaseURL(server.URL)) + require.NoError(t, err) + + model, err := provider.LanguageModel(t.Context(), "qwen/qwen3.5-flash") + require.NoError(t, err) + + _, err = model.Generate(t.Context(), fantasy.Call{ + Prompt: fantasy.Prompt{ + { + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{ + fantasy.TextPart{Text: "Hi"}, + }, + }, + }, + }) + require.NoError(t, err) + + require.Len(t, captured, 1) + assert.Equal(t, "Bearer k", captured[0]["Authorization"]) + assert.True(t, strings.HasPrefix(captured[0]["User-Agent"], "Charm-Fantasy/")) +} diff --git a/providers/atlascloud/provider_options.go b/providers/atlascloud/provider_options.go new file mode 100644 index 000000000..a6b7b2599 --- /dev/null +++ b/providers/atlascloud/provider_options.go @@ -0,0 +1,21 @@ +package atlascloud + +import ( + "charm.land/fantasy" + "charm.land/fantasy/providers/openaicompat" +) + +// ProviderOptions represents additional options for the Atlas Cloud provider. +type ProviderOptions = openaicompat.ProviderOptions + +// NewProviderOptions creates new provider options for Atlas Cloud. +func NewProviderOptions(opts *ProviderOptions) fantasy.ProviderOptions { + return fantasy.ProviderOptions{ + Name: opts, + } +} + +// ParseOptions parses provider options from a map for Atlas Cloud. +func ParseOptions(data map[string]any) (*ProviderOptions, error) { + return openaicompat.ParseOptions(data) +} diff --git a/providertests/provider_registry_test.go b/providertests/provider_registry_test.go index c0f3f848e..8ae0417a6 100644 --- a/providertests/provider_registry_test.go +++ b/providertests/provider_registry_test.go @@ -6,6 +6,7 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/anthropic" + "charm.land/fantasy/providers/atlascloud" "charm.land/fantasy/providers/google" "charm.land/fantasy/providers/openai" "charm.land/fantasy/providers/openaicompat" @@ -354,6 +355,7 @@ func TestProviderRegistry_AllTypesRegistered(t *testing.T) { {"Google Options", google.Name, &google.ProviderOptions{}}, {"OpenRouter Options", openrouter.Name, &openrouter.ProviderOptions{}}, {"OpenAICompat Options", openaicompat.Name, &openaicompat.ProviderOptions{}}, + {"AtlasCloud Options", atlascloud.Name, &atlascloud.ProviderOptions{}}, } for _, tc := range tests {