Files

1 line
174 KiB
JSON

{"repo": "andatoshiki/omni", "files": {"internal/providers/registry.go": {"f4b84845aeeebfd9a414065aac0813053b0391c7": {"t": 1785746073.314729, "n": 11101, "h": "62dd7347c9e1d88f", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n\tcohereplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cohere\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n\tconfig.ProviderTypeCohere: \"https://api.cohere.com\",\n\tconfig.ProviderTypeHuggingFace: \"https://api-inference.huggingface.co/v1\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tcase config.ProviderTypeCohere:\n\t\treturn cohereplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeHuggingFace:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// FindModelID searches enabled providers for a configured model.\n// It accepts either a bare model name when that name is unique across providers,\n// or the exact \"provider / model\" string returned by ModelID.String().\nfunc (r *Registry) FindModelID(modelName string) (ModelID, bool) {\n\tif r == nil {\n\t\treturn ModelID{}, false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn ModelID{}, false\n\t}\n\tif provider, model, ok := strings.Cut(modelName, ModelIDSeparator); ok {\n\t\tid := ModelID{Provider: provider, Model: model}\n\t\tif _, err := r.Resolve(id); err == nil {\n\t\t\treturn id, true\n\t\t}\n\t\treturn ModelID{}, false\n\t}\n\tvar match ModelID\n\tfound := false\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model == modelName {\n\t\t\tif found {\n\t\t\t\t// Reject duplicate bare-name matches so callers must use an\n\t\t\t\t// explicit provider-qualified selector instead of silently\n\t\t\t\t// routing to the first configured provider.\n\t\t\t\treturn ModelID{}, false\n\t\t\t}\n\t\t\tmatch = id\n\t\t\tfound = true\n\t\t}\n\t}\n\treturn match, found\n}\n\n// IsModelNameAmbiguous reports whether a bare model name matches more than one\n// enabled provider.\nfunc (r *Registry) IsModelNameAmbiguous(modelName string) bool {\n\tif r == nil {\n\t\treturn false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn false\n\t}\n\tmatches := 0\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model != modelName {\n\t\t\tcontinue\n\t\t}\n\t\tmatches++\n\t\tif matches > 1 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "9a9974656b633c48c2ab400734ac47c0990384e2": {"t": 1785759535.6145346, "n": 11101, "h": "62dd7347c9e1d88f", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n\tcohereplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cohere\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n\tconfig.ProviderTypeCohere: \"https://api.cohere.com\",\n\tconfig.ProviderTypeHuggingFace: \"https://api-inference.huggingface.co/v1\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tcase config.ProviderTypeCohere:\n\t\treturn cohereplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeHuggingFace:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// FindModelID searches enabled providers for a configured model.\n// It accepts either a bare model name when that name is unique across providers,\n// or the exact \"provider / model\" string returned by ModelID.String().\nfunc (r *Registry) FindModelID(modelName string) (ModelID, bool) {\n\tif r == nil {\n\t\treturn ModelID{}, false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn ModelID{}, false\n\t}\n\tif provider, model, ok := strings.Cut(modelName, ModelIDSeparator); ok {\n\t\tid := ModelID{Provider: provider, Model: model}\n\t\tif _, err := r.Resolve(id); err == nil {\n\t\t\treturn id, true\n\t\t}\n\t\treturn ModelID{}, false\n\t}\n\tvar match ModelID\n\tfound := false\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model == modelName {\n\t\t\tif found {\n\t\t\t\t// Reject duplicate bare-name matches so callers must use an\n\t\t\t\t// explicit provider-qualified selector instead of silently\n\t\t\t\t// routing to the first configured provider.\n\t\t\t\treturn ModelID{}, false\n\t\t\t}\n\t\t\tmatch = id\n\t\t\tfound = true\n\t\t}\n\t}\n\treturn match, found\n}\n\n// IsModelNameAmbiguous reports whether a bare model name matches more than one\n// enabled provider.\nfunc (r *Registry) IsModelNameAmbiguous(modelName string) bool {\n\tif r == nil {\n\t\treturn false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn false\n\t}\n\tmatches := 0\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model != modelName {\n\t\t\tcontinue\n\t\t}\n\t\tmatches++\n\t\tif matches > 1 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "e96b0da042e13d32a91c4503ce0dd43b3bce47e0": {"t": 1785759537.8373237, "n": 10086, "h": "d306b31cf9c6d202", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n\tcohereplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cohere\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n\tconfig.ProviderTypeCohere: \"https://api.cohere.com\",\n\tconfig.ProviderTypeHuggingFace: \"https://api-inference.huggingface.co/v1\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tcase config.ProviderTypeCohere:\n\t\treturn cohereplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeHuggingFace:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// FindModelID searches all enabled providers for a model matching the exact name.\n// Returns the first match found based on config order.\nfunc (r *Registry) FindModelID(modelName string) (ModelID, bool) {\n\tif r == nil {\n\t\treturn ModelID{}, false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn ModelID{}, false\n\t}\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model == modelName {\n\t\t\treturn id, true\n\t\t}\n\t}\n\treturn ModelID{}, false\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "520c0e2098c3612039dcac90a916bc289831ea13": {"t": 1785759539.1269307, "n": 10683, "h": "42e55841fe2da2bf", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// FindModelID searches enabled providers for a configured model.\n// It accepts either a bare model name when that name is unique across providers,\n// or the exact \"provider / model\" string returned by ModelID.String().\nfunc (r *Registry) FindModelID(modelName string) (ModelID, bool) {\n\tif r == nil {\n\t\treturn ModelID{}, false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn ModelID{}, false\n\t}\n\tif provider, model, ok := strings.Cut(modelName, ModelIDSeparator); ok {\n\t\tid := ModelID{Provider: provider, Model: model}\n\t\tif _, err := r.Resolve(id); err == nil {\n\t\t\treturn id, true\n\t\t}\n\t\treturn ModelID{}, false\n\t}\n\tvar match ModelID\n\tfound := false\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model == modelName {\n\t\t\tif found {\n\t\t\t\t// Reject duplicate bare-name matches so callers must use an\n\t\t\t\t// explicit provider-qualified selector instead of silently\n\t\t\t\t// routing to the first configured provider.\n\t\t\t\treturn ModelID{}, false\n\t\t\t}\n\t\t\tmatch = id\n\t\t\tfound = true\n\t\t}\n\t}\n\treturn match, found\n}\n\n// IsModelNameAmbiguous reports whether a bare model name matches more than one\n// enabled provider.\nfunc (r *Registry) IsModelNameAmbiguous(modelName string) bool {\n\tif r == nil {\n\t\treturn false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn false\n\t}\n\tmatches := 0\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model != modelName {\n\t\t\tcontinue\n\t\t}\n\t\tmatches++\n\t\tif matches > 1 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "2e960c7c5c84497c31ccc1c96b823306acd87f58": {"t": 1785759546.1196322, "n": 10724, "h": "709700dcb6442fe5", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// FindModelID searches enabled providers for a configured model.\n// It accepts either a bare model name when that name is unique across providers,\n// or the exact \"provider / model\" string returned by ModelID.String().\nfunc (r *Registry) FindModelID(modelName string) (ModelID, bool) {\n\tif r == nil {\n\t\treturn ModelID{}, false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn ModelID{}, false\n\t}\n\tif provider, model, ok := strings.Cut(modelName, ModelIDSeparator); ok {\n\t\tid := ModelID{Provider: strings.TrimSpace(provider), Model: strings.TrimSpace(model)}\n\t\tif _, err := r.Resolve(id); err == nil {\n\t\t\treturn id, true\n\t\t}\n\t\treturn ModelID{}, false\n\t}\n\tvar match ModelID\n\tfound := false\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model == modelName {\n\t\t\tif found {\n\t\t\t\t// Reject duplicate bare-name matches so callers must use an\n\t\t\t\t// explicit provider-qualified selector instead of silently\n\t\t\t\t// routing to the first configured provider.\n\t\t\t\treturn ModelID{}, false\n\t\t\t}\n\t\t\tmatch = id\n\t\t\tfound = true\n\t\t}\n\t}\n\treturn match, found\n}\n\n// IsModelNameAmbiguous reports whether a bare model name matches more than one\n// enabled provider.\nfunc (r *Registry) IsModelNameAmbiguous(modelName string) bool {\n\tif r == nil {\n\t\treturn false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn false\n\t}\n\tmatches := 0\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model != modelName {\n\t\t\tcontinue\n\t\t}\n\t\tmatches++\n\t\tif matches > 1 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "6ce793b22393a33bf866477ae7b0215fc7cbb54f": {"t": 1785759547.6215303, "n": 10098, "h": "673825b5275addcc", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// FindModelID searches enabled providers for a configured model.\n// It accepts either a bare model name when that name is unique across providers,\n// or the exact \"provider / model\" string returned by ModelID.String().\nfunc (r *Registry) FindModelID(modelName string) (ModelID, bool) {\n\tif r == nil {\n\t\treturn ModelID{}, false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn ModelID{}, false\n\t}\n\tif provider, model, ok := strings.Cut(modelName, ModelIDSeparator); ok {\n\t\tid := ModelID{Provider: strings.TrimSpace(provider), Model: strings.TrimSpace(model)}\n\t\tif _, err := r.Resolve(id); err == nil {\n\t\t\treturn id, true\n\t\t}\n\t\treturn ModelID{}, false\n\t}\n\tvar match ModelID\n\tfound := false\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model == modelName {\n\t\t\tif found {\n\t\t\t\treturn ModelID{}, false\n\t\t\t}\n\t\t\tmatch = id\n\t\t\tfound = true\n\t\t}\n\t}\n\treturn match, found\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "762a4a1f09a243321b436dec3189349ad36bfa33": {"t": 1785759549.3861995, "n": 10087, "h": "527c58a8b83051ef", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// FindModelID searches enabled providers for a configured model.\n// It accepts either a bare model name when that name is unique across providers,\n// or the exact \"provider / model\" string returned by ModelID.String().\nfunc (r *Registry) FindModelID(modelName string) (ModelID, bool) {\n\tif r == nil {\n\t\treturn ModelID{}, false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn ModelID{}, false\n\t}\n\tif provider, model, ok := strings.Cut(modelName, \" / \"); ok {\n\t\tid := ModelID{Provider: strings.TrimSpace(provider), Model: strings.TrimSpace(model)}\n\t\tif _, err := r.Resolve(id); err == nil {\n\t\t\treturn id, true\n\t\t}\n\t\treturn ModelID{}, false\n\t}\n\tvar match ModelID\n\tfound := false\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model == modelName {\n\t\t\tif found {\n\t\t\t\treturn ModelID{}, false\n\t\t\t}\n\t\t\tmatch = id\n\t\t\tfound = true\n\t\t}\n\t}\n\treturn match, found\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "f482aefe0f7abefbc09a12e52e44ea9c04936a17": {"t": 1785759550.7450736, "n": 10071, "h": "8b53eb718cc430f0", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n\tcohereplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cohere\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n\tconfig.ProviderTypeCohere: \"https://api.cohere.com/v2\",\n\tconfig.ProviderTypeHuggingFace:\"https://api-inference.huggingface.co/v1\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tcase config.ProviderTypeCohere:\n\t\treturn cohereplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeHuggingFace:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// FindModelID searches all enabled providers for a model matching the exact name.\n// Returns the first match found based on config order.\nfunc (r *Registry) FindModelID(modelName string) (ModelID, bool) {\n\tif r == nil {\n\t\treturn ModelID{}, false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn ModelID{}, false\n\t}\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model == modelName {\n\t\t\treturn id, true\n\t\t}\n\t}\n\treturn ModelID{}, false\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "6b153ceee9ab8ef087a7ba6cf931a415bc239f92": {"t": 1785759553.497421, "n": 9668, "h": "d44bca92092c4f1f", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// FindModelID searches all enabled providers for a model matching the exact name.\n// Returns the first match found based on config order.\nfunc (r *Registry) FindModelID(modelName string) (ModelID, bool) {\n\tif r == nil {\n\t\treturn ModelID{}, false\n\t}\n\tmodelName = strings.TrimSpace(modelName)\n\tif modelName == \"\" {\n\t\treturn ModelID{}, false\n\t}\n\tfor _, id := range r.AllModelIDs() {\n\t\tif id.Model == modelName {\n\t\t\treturn id, true\n\t\t}\n\t}\n\treturn ModelID{}, false\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "4e576ffa40253b428855286c10d485f86588af6d": {"t": 1785759555.8912258, "n": 9204, "h": "6f8de44d20c52a61", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n\tazureplatform \"github.com/andatoshiki/omni/internal/providers/platforms/azureopenai\"\n\tcloudflareplatform \"github.com/andatoshiki/omni/internal/providers/platforms/cloudflare\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n\tconfig.ProviderTypeAzure: \"https://%s.openai.azure.com\",\n\tconfig.ProviderTypeCloudflare: \"https://api.cloudflare.com/client/v4/accounts/%s/ai/run\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tcase config.ProviderTypeAzure:\n\t\treturn azureplatform.Adapter{APIVersion: cfg.APIVersion, HTTPClient: client}, nil\n\tcase config.ProviderTypeCloudflare:\n\t\treturn cloudflareplatform.Adapter{AccountID: cfg.CloudflareAccountID, HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "b97e0f6b75b4fc29231d941ecb7ac18c0315fe91": {"t": 1785759557.401025, "n": 8624, "h": "e02805b82fbce8b8", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\tgroqplatform \"github.com/andatoshiki/omni/internal/providers/platforms/groq\"\n\tmistralplatform \"github.com/andatoshiki/omni/internal/providers/platforms/mistral\"\n\tollamaplatform \"github.com/andatoshiki/omni/internal/providers/platforms/ollama\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\ttogetherplatform \"github.com/andatoshiki/omni/internal/providers/platforms/together\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn ollamaplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn groqplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn togetherplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn mistralplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "9e75333f47637844935f85bc1c12a56bd759c89d": {"t": 1785759558.7531042, "n": 8165, "h": "6ba98d0d81344376", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/bedrockruntime\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms/bedrock\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(cfg config.ProviderConfig) (Adapter, error) {\n\tproviderType := cfg.EffectiveType()\n\tvar client *http.Client\n\tif cfg.Timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *cfg.Timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeBedrock:\n\t\tctx := context.Background()\n\t\tvar awsCfg aws.Config\n\t\tvar err error\n\n\t\tif cfg.AWSAccessKey != \"\" && cfg.AWSSecretKey != \"\" {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx,\n\t\t\t\tawsconfig.WithRegion(cfg.AWSRegion),\n\t\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWSAccessKey, cfg.AWSSecretKey, \"\")),\n\t\t\t)\n\t\t} else {\n\t\t\tawsCfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.AWSRegion))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load bedrock aws config: %w\", err)\n\t\t}\n\n\t\tif client != nil {\n\t\t\tawsCfg.HTTPClient = client\n\t\t}\n\n\t\tbedrockClient := bedrockruntime.NewFromConfig(awsCfg)\n\t\treturn bedrock.Adapter{Client: bedrockClient}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "f657d8b3c7d9cdd8e85e0074448e6a263fc3560d": {"t": 1785759560.0932693, "n": 7185, "h": "c5cf26c11df40700", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n\tconfig.ProviderTypeOllama: \"http://localhost:11434/v1\",\n\tconfig.ProviderTypeGroq: \"https://api.groq.com/openai/v1\",\n\tconfig.ProviderTypeTogether: \"https://api.together.xyz/v1\",\n\tconfig.ProviderTypeMistral: \"https://api.mistral.ai/v1\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(providerType, cfg.Timeout)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(providerType string, timeout *time.Duration) (Adapter, error) {\n\tvar client *http.Client\n\tif timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeOllama:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeGroq:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeTogether:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeMistral:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "f8e7d6b4cc80802ec3033f9b70ad35f27fb1473a": {"t": 1785759561.5440748, "n": 6573, "h": "1e2ea4fa83367a56", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(providerType, cfg.Timeout)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(providerType string, timeout *time.Duration) (Adapter, error) {\n\tvar client *http.Client\n\tif timeout != nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\ttransport.ResponseHeaderTimeout = *timeout\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "916a1c1fd2457242ec199934ce1c0092271f9e9d": {"t": 1785759562.7836065, "n": 6460, "h": "90ddcd29b16f3bd8", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n\tconfig.ProviderTypePerplexity: \"https://api.perplexity.ai\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(providerType, cfg.Timeout)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(providerType string, timeout *time.Duration) (Adapter, error) {\n\tvar client *http.Client\n\tif timeout != nil {\n\t\tclient = &http.Client{Timeout: *timeout}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{Timeout: timeout}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypePerplexity:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "c8bf62d48012dfbb64cf266a62543933f8a008ca": {"t": 1785759564.1283157, "n": 6302, "h": "bfc9ff4ec500eed5", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n\txaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/xai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n\tconfig.ProviderTypeXAI: \"https://api.x.ai/v1\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(providerType, cfg.Timeout)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(providerType string, timeout *time.Duration) (Adapter, error) {\n\tvar client *http.Client\n\tif timeout != nil {\n\t\tclient = &http.Client{Timeout: *timeout}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{Timeout: timeout}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeXAI:\n\t\treturn xaiplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "80505407659866eee27aaee414e815f04f9c76d6": {"t": 1785759565.3476458, "n": 6056, "h": "29700b519273a224", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tanthropicplatform \"github.com/andatoshiki/omni/internal/providers/platforms/anthropic\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n\tconfig.ProviderTypeAnthropic: \"https://api.anthropic.com\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(providerType, cfg.Timeout)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(providerType string, timeout *time.Duration) (Adapter, error) {\n\tvar client *http.Client\n\tif timeout != nil {\n\t\tclient = &http.Client{Timeout: *timeout}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{Timeout: timeout}, nil\n\tcase config.ProviderTypeAnthropic:\n\t\treturn anthropicplatform.Adapter{HTTPClient: client}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "4d81c5dbb03fe4876ac70dc76e566d18a7d57f99": {"t": 1785759567.1760504, "n": 5808, "h": "361dc0e17552349e", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\tgoogleplatform \"github.com/andatoshiki/omni/internal/providers/platforms/google\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeGoogle: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(providerType, cfg.Timeout)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(providerType string, timeout *time.Duration) (Adapter, error) {\n\tvar client *http.Client\n\tif timeout != nil {\n\t\tclient = &http.Client{Timeout: *timeout}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeGoogle:\n\t\treturn googleplatform.Adapter{Timeout: timeout}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}, "d6b9fa6d910e50e5c30ce8d2e6914f8eca9bb8c7": {"t": 1785759572.0319397, "n": 5550, "h": "3bbf625e8075b16b", "c": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/andatoshiki/omni/internal/config\"\n\t\"github.com/andatoshiki/omni/internal/providers/platforms\"\n\tcustomplatform \"github.com/andatoshiki/omni/internal/providers/platforms/custom\"\n\tdeepseekplatform \"github.com/andatoshiki/omni/internal/providers/platforms/deepseek\"\n\topenaiplatform \"github.com/andatoshiki/omni/internal/providers/platforms/openai\"\n)\n\n// Provider holds the runtime configuration for a single AI provider.\ntype Provider struct {\n\tName string\n\tType string\n\tAPIKey string\n\tBaseURL string\n\tModels []config.ModelConfig\n\n\tadapter Adapter\n}\n\n// Adapter is the boundary between bot commands and provider-specific HTTP APIs.\ntype Adapter interface {\n\tCreateChatCompletionStream(ctx context.Context, endpoint platforms.Endpoint, request *platforms.ChatCompletionStreamRequest) (*platforms.ChatCompletionStream, error)\n}\n\n// Registry holds all enabled providers, keyed by provider name.\ntype Registry struct {\n\tproviders map[string]*Provider\n\torder []string\n}\n\nvar defaultBaseURLs = map[string]string{\n\tconfig.ProviderTypeDeepSeek: \"https://api.deepseek.com\",\n\tconfig.ProviderTypeOpenAI: \"https://api.openai.com/v1\",\n\tconfig.ProviderTypeCustom: \"https://api.openai.com/v1\",\n}\n\n// NewRegistry initializes the provider registry from config.\n// Only enabled providers are registered.\nfunc NewRegistry(configs []config.ProviderConfig) (*Registry, error) {\n\tregistry := &Registry{\n\t\tproviders: make(map[string]*Provider, len(configs)),\n\t\torder: make([]string, 0, len(configs)),\n\t}\n\n\tfor _, cfg := range configs {\n\t\tif !cfg.IsEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tproviderType := cfg.EffectiveType()\n\t\tadapter, err := adapterForType(providerType, cfg.Timeout)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"provider %q: %w\", cfg.Name, err)\n\t\t}\n\n\t\tbaseURL := strings.TrimSpace(cfg.APIBase)\n\t\tif baseURL == \"\" {\n\t\t\tbaseURL = defaultBaseURLs[providerType]\n\t\t}\n\n\t\tprovider := &Provider{\n\t\t\tName: strings.TrimSpace(cfg.Name),\n\t\t\tType: providerType,\n\t\t\tAPIKey: strings.TrimSpace(cfg.APIKey),\n\t\t\tBaseURL: strings.TrimRight(baseURL, \"/\"),\n\t\t\tModels: cfg.Models,\n\t\t\tadapter: adapter,\n\t\t}\n\t\tregistry.providers[provider.Name] = provider\n\t\tregistry.order = append(registry.order, provider.Name)\n\t}\n\n\treturn registry, nil\n}\n\nfunc adapterForType(providerType string, timeout *time.Duration) (Adapter, error) {\n\tvar client *http.Client\n\tif timeout != nil {\n\t\tclient = &http.Client{Timeout: *timeout}\n\t}\n\tswitch providerType {\n\tcase config.ProviderTypeDeepSeek:\n\t\treturn deepseekplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tcase config.ProviderTypeOpenAI:\n\t\treturn openaiplatform.Adapter{HTTPClient: client}, nil\n\tcase config.ProviderTypeCustom:\n\t\treturn customplatform.Adapter{OpenAI: openaiplatform.Adapter{HTTPClient: client}}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provider type %q\", providerType)\n\t}\n}\n\n// ProviderNames returns enabled provider names in deterministic display order.\nfunc (r *Registry) ProviderNames() []string {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnames := slices.Clone(r.order)\n\tslices.Sort(names)\n\treturn names\n}\n\nfunc (r *Registry) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\treturn len(r.providers)\n}\n\n// DefaultModelID returns the first model of the first enabled provider\n// in config order.\nfunc (r *Registry) DefaultModelID() ModelID {\n\tif r == nil {\n\t\treturn ModelID{}\n\t}\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider != nil && len(provider.Models) > 0 {\n\t\t\treturn ModelID{\n\t\t\t\tProvider: provider.Name,\n\t\t\t\tModel: provider.Models[0].Name,\n\t\t\t}\n\t\t}\n\t}\n\treturn ModelID{}\n}\n\n// Resolve returns the Provider for a given ModelID, validating that both\n// provider and model are configured and enabled.\nfunc (r *Registry) Resolve(id ModelID) (*Provider, error) {\n\tif r == nil {\n\t\treturn nil, fmt.Errorf(\"provider registry is not initialized\")\n\t}\n\tprovider, ok := r.providers[id.Provider]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown provider: %s\", id.Provider)\n\t}\n\tfor _, m := range provider.Models {\n\t\tif m.Name == id.Model {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"model %q not configured for provider %q\", id.Model, id.Provider)\n}\n\n// CreateChatCompletionStream opens a streaming chat completion response for\n// the selected provider and model.\nfunc (r *Registry) CreateChatCompletionStream(ctx context.Context, id ModelID, request *ChatCompletionStreamRequest) (*ChatCompletionStream, error) {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn provider.adapter.CreateChatCompletionStream(ctx, platforms.Endpoint{\n\t\tAPIKey: provider.APIKey,\n\t\tBaseURL: provider.BaseURL,\n\t}, request)\n}\n\n// AllModelIDs returns all configured model IDs from enabled providers\n// in config order.\nfunc (r *Registry) AllModelIDs() []ModelID {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tvar ids []ModelID\n\tfor _, providerName := range r.order {\n\t\tprovider := r.providers[providerName]\n\t\tif provider == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, model := range provider.Models {\n\t\t\tids = append(ids, ModelID{Provider: provider.Name, Model: model.Name})\n\t\t}\n\t}\n\treturn ids\n}\n\n// LookupModelConfig returns the configured pricing for a given ModelID,\n// or nil if not found.\nfunc (r *Registry) LookupModelConfig(id ModelID) *config.ModelConfig {\n\tprovider, err := r.Resolve(id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range provider.Models {\n\t\tif provider.Models[i].Name == id.Model {\n\t\t\treturn &provider.Models[i]\n\t\t}\n\t}\n\treturn nil\n}\n"}}}}