diff --git a/cmd/gateway_providers.go b/cmd/gateway_providers.go index c3e4418d..80702bfb 100644 --- a/cmd/gateway_providers.go +++ b/cmd/gateway_providers.go @@ -146,6 +146,30 @@ func registerProviders(registry *providers.Registry, cfg *config.Config) { slog.Info("registered provider", "name", "novita") } + // BytePlus ModelArk — OpenAI-compatible (standard Bearer auth). + if cfg.Providers.BytePlus.APIKey != "" { + base := cfg.Providers.BytePlus.APIBase + if base == "" { + base = store.BytePlusDefaultAPIBase + } + prov := providers.NewOpenAIProvider("byteplus", cfg.Providers.BytePlus.APIKey, base, store.BytePlusDefaultModel) + prov.WithProviderType(store.ProviderBytePlus) + registry.Register(prov) + slog.Info("registered provider", "name", "byteplus") + } + + // BytePlus ModelArk Coding Plan — separate endpoint for developer tools quota. + if cfg.Providers.BytePlusCoding.APIKey != "" { + base := cfg.Providers.BytePlusCoding.APIBase + if base == "" { + base = store.BytePlusCodingDefaultAPIBase + } + prov := providers.NewOpenAIProvider("byteplus-coding", cfg.Providers.BytePlusCoding.APIKey, base, store.BytePlusDefaultModel) + prov.WithProviderType(store.ProviderBytePlusCoding) + registry.Register(prov) + slog.Info("registered provider", "name", "byteplus-coding") + } + // Claude CLI provider (subscription-based, no API key needed) if cfg.Providers.ClaudeCLI.CLIPath != "" { cliPath := cfg.Providers.ClaudeCLI.CLIPath @@ -358,6 +382,22 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi base = store.NovitaDefaultAPIBase } registry.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.NovitaDefaultModel)) + case store.ProviderBytePlus: + base := p.APIBase + if base == "" { + base = store.BytePlusDefaultAPIBase + } + prov := providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.BytePlusDefaultModel) + prov.WithProviderType(p.ProviderType) + registry.RegisterForTenant(p.TenantID, prov) + case store.ProviderBytePlusCoding: + base := p.APIBase + if base == "" { + base = store.BytePlusCodingDefaultAPIBase + } + prov := providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.BytePlusDefaultModel) + prov.WithProviderType(p.ProviderType) + registry.RegisterForTenant(p.TenantID, prov) default: prov := providers.NewOpenAIProvider(p.Name, p.APIKey, p.APIBase, "") prov.WithProviderType(p.ProviderType) diff --git a/docs/02-providers.md b/docs/02-providers.md index 937db8dc..6c9567b7 100644 --- a/docs/02-providers.md +++ b/docs/02-providers.md @@ -1,6 +1,6 @@ # 02 - LLM Providers -GoClaw abstracts LLM communication behind a single `Provider` interface, allowing the agent loop to work with any backend without knowing the wire format. Six concrete implementations exist: Anthropic (native HTTP+SSE), OpenAI-compatible (covering 10+ API endpoints), Claude CLI (local binary), Codex (OAuth-based), ACP (subagent orchestration), and DashScope (Alibaba Qwen with thinking). +GoClaw abstracts LLM communication behind a single `Provider` interface, allowing the agent loop to work with any backend without knowing the wire format. Six concrete implementations exist: Anthropic (native HTTP+SSE), OpenAI-compatible (covering 10+ API endpoints), Claude CLI (local binary), Codex (OAuth-based), ACP (subagent orchestration), and DashScope (Alibaba Qwen with thinking). The OpenAI-compatible provider also supports BytePlus ModelArk (Seed 2.0 models with image/video generation). --- @@ -75,6 +75,8 @@ All HTTP-based providers (Anthropic, OpenAI-compatible, Codex) use 300-second ti | bailian | `https://coding-intl.dashscope.aliyuncs.com/v1` | `qwen3.5-plus` | Alibaba Coding API | | zai | `https://api.z.ai/api/paas/v4` | `glm-5` | | | zai-coding | `https://api.z.ai/api/coding/paas/v4` | `glm-5` | | +| byteplus | `https://ark.ap-southeast.bytepluses.com/api/v3` | `seed-2-0-lite-260228` | Seed 2.0 models | +| byteplus_coding | `https://ark.ap-southeast.bytepluses.com/api/coding/v3` | `seed-2-0-lite-260228` | Seed 2.0 Coding Plan | --- @@ -736,6 +738,8 @@ Reasoning behavior: | `internal/providers/schema_cleaner.go` | CleanSchemaForProvider, CleanToolSchemas, recursive schema field removal | | `internal/providers/registry.go` | Provider registry: registration, lookup, lifecycle management | | `cmd/gateway_providers.go` | Provider registration from config and database during gateway startup | +| `internal/tools/create_image_byteplus.go` | BytePlus Seedream async image generation (async polling) | +| `internal/tools/create_video_byteplus.go` | BytePlus Seedance async video generation (async polling, 2K resolution) | --- diff --git a/internal/config/config_channels.go b/internal/config/config_channels.go index db310a85..a45de45a 100644 --- a/internal/config/config_channels.go +++ b/internal/config/config_channels.go @@ -213,7 +213,9 @@ type ProvidersConfig struct { OllamaCloud ProviderConfig `json:"ollama_cloud"` // Ollama Cloud (API key required) ClaudeCLI ClaudeCLIConfig `json:"claude_cli"` ACP ACPConfig `json:"acp"` - Novita ProviderConfig `json:"novita"` // Novita AI (OpenAI-compatible endpoint) + Novita ProviderConfig `json:"novita"` // Novita AI (OpenAI-compatible endpoint) + BytePlus ProviderConfig `json:"byteplus"` // BytePlus ModelArk (Seed 2.0) + BytePlusCoding ProviderConfig `json:"byteplus_coding"` // BytePlus ModelArk Coding Plan } // OllamaConfig configures a local (or self-hosted) Ollama instance. @@ -284,6 +286,10 @@ func (p *ProvidersConfig) APIBaseForType(providerType string) string { return p.OllamaCloud.APIBase case "novita": return p.Novita.APIBase + case "byteplus": + return p.BytePlus.APIBase + case "byteplus_coding": + return p.BytePlusCoding.APIBase default: return "" } @@ -311,7 +317,9 @@ func (c *Config) HasAnyProvider() bool { p.OllamaCloud.APIKey != "" || p.ClaudeCLI.CLIPath != "" || p.ACP.Binary != "" || - p.Novita.APIKey != "" + p.Novita.APIKey != "" || + p.BytePlus.APIKey != "" || + p.BytePlusCoding.APIKey != "" } // QuotaWindow defines request limits per time window. Zero means unlimited. diff --git a/internal/providers/openai.go b/internal/providers/openai.go index 67dc882e..fcd057f4 100644 --- a/internal/providers/openai.go +++ b/internal/providers/openai.go @@ -24,6 +24,7 @@ type OpenAIProvider struct { apiKey string apiBase string chatPath string // defaults to "/chat/completions" + authPrefix string // auth header prefix, defaults to "Bearer " if empty defaultModel string providerType string // DB provider_type (e.g. "gemini_native", "openai", "minimax_native") client *http.Client @@ -106,11 +107,19 @@ func (p *OpenAIProvider) WithChatPath(path string) *OpenAIProvider { return p } +// WithAuthPrefix sets a custom Authorization header prefix for providers with non-standard auth formats. +// Default is "Bearer " if not set. +func (p *OpenAIProvider) WithAuthPrefix(prefix string) *OpenAIProvider { + p.authPrefix = prefix + return p +} + func (p *OpenAIProvider) Name() string { return p.name } func (p *OpenAIProvider) DefaultModel() string { return p.defaultModel } func (p *OpenAIProvider) SupportsThinking() bool { return true } func (p *OpenAIProvider) APIKey() string { return p.apiKey } func (p *OpenAIProvider) APIBase() string { return p.apiBase } +func (p *OpenAIProvider) AuthPrefix() string { return p.authPrefix } func (p *OpenAIProvider) ProviderType() string { return p.providerType } // schemaProviderName returns the most specific provider identifier for schema normalization. @@ -561,7 +570,11 @@ func (p *OpenAIProvider) doRequest(ctx context.Context, body any) (io.ReadCloser if strings.Contains(strings.ToLower(p.apiBase), "azure.com") { httpReq.Header.Set("api-key", p.apiKey) } else { - httpReq.Header.Set("Authorization", "Bearer "+p.apiKey) + prefix := p.authPrefix + if prefix == "" { + prefix = "Bearer " + } + httpReq.Header.Set("Authorization", prefix+p.apiKey) } resp, err := p.client.Do(httpReq) diff --git a/internal/store/provider_store.go b/internal/store/provider_store.go index 9dce54b3..e3ec22c4 100644 --- a/internal/store/provider_store.go +++ b/internal/store/provider_store.go @@ -31,11 +31,18 @@ const ( ProviderOllama = "ollama" // local or self-hosted Ollama (no API key) ProviderOllamaCloud = "ollama_cloud" // Ollama Cloud (Bearer token required) ProviderACP = "acp" // ACP (Agent Client Protocol) agent subprocess - ProviderNovita = "novita" // Novita AI (OpenAI-compatible endpoint) + ProviderNovita = "novita" // Novita AI (OpenAI-compatible endpoint) + ProviderBytePlus = "byteplus" // BytePlus ModelArk (Seed 2.0 models) + ProviderBytePlusCoding = "byteplus_coding" // BytePlus ModelArk Coding Plan // Novita AI defaults. NovitaDefaultAPIBase = "https://api.novita.ai/openai" NovitaDefaultModel = "moonshotai/kimi-k2.5" + + // BytePlus ModelArk defaults. + BytePlusDefaultAPIBase = "https://ark.ap-southeast.bytepluses.com/api/v3" + BytePlusCodingDefaultAPIBase = "https://ark.ap-southeast.bytepluses.com/api/coding/v3" + BytePlusDefaultModel = "seed-2-0-lite-260228" ) // ValidProviderTypes lists all accepted provider_type values. @@ -63,6 +70,8 @@ var ValidProviderTypes = map[string]bool{ ProviderOllamaCloud: true, ProviderACP: true, ProviderNovita: true, + ProviderBytePlus: true, + ProviderBytePlusCoding: true, } // LLMProviderData represents an LLM provider configuration. diff --git a/internal/tools/create_image.go b/internal/tools/create_image.go index 2b978a74..7f1dde17 100644 --- a/internal/tools/create_image.go +++ b/internal/tools/create_image.go @@ -25,7 +25,7 @@ type credentialProvider interface { } // imageGenProviderPriority is the default order for image generation providers. -var imageGenProviderPriority = []string{"openrouter", "gemini", "openai", "minimax", "dashscope"} +var imageGenProviderPriority = []string{"openrouter", "gemini", "openai", "minimax", "dashscope", "byteplus"} // imageGenModelDefaults maps provider names to default image generation models. var imageGenModelDefaults = map[string]string{ @@ -34,6 +34,7 @@ var imageGenModelDefaults = map[string]string{ "gemini": "gemini-2.5-flash-image", "minimax": "image-01", "dashscope": "wan2.6-image", + "byteplus": "seedream-5-0-260128", } // CreateImageTool generates images using an image generation API. @@ -153,6 +154,8 @@ func (t *CreateImageTool) callProvider(ctx context.Context, cp credentialProvide return callMinimaxImageGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, params) case "dashscope": return callDashScopeImageGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, params) + case "byteplus": + return callBytePlusImageGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, params) default: return t.callStandardImageGenAPI(ctx, cp.APIKey(), cp.APIBase(), model, prompt, params) } diff --git a/internal/tools/create_image_byteplus.go b/internal/tools/create_image_byteplus.go new file mode 100644 index 00000000..01f69c51 --- /dev/null +++ b/internal/tools/create_image_byteplus.go @@ -0,0 +1,114 @@ +package tools + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "strings" + + "github.com/nextlevelbuilder/goclaw/internal/providers" +) + +// bytePlusImageEndpoint derives the Seedream image generation endpoint from the stored api_base. +// Media endpoints always use the standard /api/v3 path (not /api/coding/v3). +func bytePlusImageEndpoint(apiBase string) string { + return bytePlusMediaBase(apiBase) + "/images/generations" +} + +// bytePlusMediaBase extracts the host and returns the standard /api/v3 media base. +// Strips /chat/completions, /coding/v3, etc. to always use /api/v3 for media endpoints. +func bytePlusMediaBase(apiBase string) string { + base := strings.TrimRight(apiBase, "/") + base = strings.TrimSuffix(base, "/chat/completions") + // Strip any versioned path to rebuild consistently + for _, suffix := range []string{"/api/coding/v3", "/api/v3", "/v3"} { + if before, ok := strings.CutSuffix(base, suffix); ok { + return before + "/api/v3" + } + } + return base + "/api/v3" +} + +// callBytePlusImageGen calls the BytePlus Seedream image generation API. +// Seedream returns results synchronously (no async polling needed). +// Endpoint: POST /api/v3/images/generations +func callBytePlusImageGen(ctx context.Context, apiKey, apiBase, model, prompt string, params map[string]any) ([]byte, *providers.Usage, error) { + size := aspectRatioToBytePlusSize(params) + endpoint := bytePlusImageEndpoint(apiBase) + + body := map[string]any{ + "model": model, + "prompt": prompt, + "size": size, + "response_format": "url", + } + + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, nil, fmt.Errorf("marshal request: %w", err) + } + + slog.Info("create_image: calling BytePlus Seedream API", "model", model, "size", size) + + req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(jsonBody)) + if err != nil { + return nil, nil, fmt.Errorf("create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("http request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("read response: %w", err) + } + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("API error %d: %s", resp.StatusCode, truncateBytes(respBody, 500)) + } + + var result struct { + Data []struct { + URL string `json:"url"` + } `json:"data"` + } + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, nil, fmt.Errorf("parse response: %w", err) + } + + if len(result.Data) == 0 || result.Data[0].URL == "" { + return nil, nil, fmt.Errorf("no image URL in BytePlus response: %s", truncateBytes(respBody, 300)) + } + + return downloadImageURL(ctx, result.Data[0].URL) +} + +// aspectRatioToBytePlusSize converts aspect_ratio to BytePlus size format. +// Seedream supports "1k", "2K", "4K" or "WIDTHxHEIGHT". +func aspectRatioToBytePlusSize(params map[string]any) string { + if s := GetParamString(params, "size", ""); s != "" { + return s + } + ar := GetParamString(params, "aspect_ratio", "1:1") + switch ar { + case "16:9": + return "1280x720" + case "9:16": + return "720x1280" + case "4:3": + return "1024x768" + case "3:4": + return "768x1024" + default: + return "1024x1024" + } +} diff --git a/internal/tools/create_video.go b/internal/tools/create_video.go index a0f6f752..c63a90a7 100644 --- a/internal/tools/create_video.go +++ b/internal/tools/create_video.go @@ -15,13 +15,14 @@ import ( ) // videoGenProviderPriority is the default order for video generation providers. -var videoGenProviderPriority = []string{"gemini", "minimax", "openrouter"} +var videoGenProviderPriority = []string{"gemini", "minimax", "openrouter", "byteplus"} // videoGenModelDefaults maps provider names to default video generation models. var videoGenModelDefaults = map[string]string{ "gemini": "veo-3.1-lite-generate-preview", "minimax": "MiniMax-Hailuo-2.3", "openrouter": "google/veo-3.1-lite-generate-preview", + "byteplus": "seedance-1-5-pro", } // maxImageToVideoBytes is the maximum image file size for image-to-video (20 MB). @@ -218,6 +219,8 @@ func (t *CreateVideoTool) callProvider(ctx context.Context, cp credentialProvide return t.callGeminiVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, duration, aspectRatio, params) case "minimax": return callMinimaxVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, params) + case "byteplus": + return callBytePlusVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, duration, aspectRatio, params) default: return t.callChatVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, duration, aspectRatio, params) } diff --git a/internal/tools/create_video_byteplus.go b/internal/tools/create_video_byteplus.go new file mode 100644 index 00000000..25c7d524 --- /dev/null +++ b/internal/tools/create_video_byteplus.go @@ -0,0 +1,183 @@ +package tools + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "time" + + "github.com/nextlevelbuilder/goclaw/internal/providers" +) + +// bytePlusVideoRunEndpoint derives the Seedance video generation submit endpoint. +// Media endpoints always use the standard /api/v3 path (not /api/coding/v3). +func bytePlusVideoRunEndpoint(apiBase string) string { + return bytePlusMediaBase(apiBase) + "/text-to-video-pro/run" +} + +// bytePlusVideoStatusEndpoint derives the Seedance video generation poll endpoint. +func bytePlusVideoStatusEndpoint(apiBase, taskID string) string { + return bytePlusMediaBase(apiBase) + "/text-to-video-pro/status/" + taskID +} + +// callBytePlusVideoGen calls the BytePlus Seedance video generation API. +// The API is async: POST /text-to-video-pro/run → task id → poll /status/{id}. +func callBytePlusVideoGen(ctx context.Context, apiKey, apiBase, model, prompt string, duration int, aspectRatio string, params map[string]any) ([]byte, *providers.Usage, error) { + endpoint := bytePlusVideoRunEndpoint(apiBase) + + input := map[string]any{ + "prompt": prompt, + } + if aspectRatio != "" { + input["ratio"] = aspectRatio + } + if duration > 0 { + input["duration"] = fmt.Sprintf("%d", duration) + } + resolution := GetParamString(params, "resolution", "480p") + input["resolution"] = resolution + + body := map[string]any{ + "input": input, + } + + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, nil, fmt.Errorf("marshal request: %w", err) + } + + slog.Info("create_video: calling BytePlus Seedance API", + "model", model, "duration", duration, "ratio", aspectRatio, "resolution", resolution) + + req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(jsonBody)) + if err != nil { + return nil, nil, fmt.Errorf("create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("http request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("read response: %w", err) + } + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("API error %d: %s", resp.StatusCode, truncateBytes(respBody, 500)) + } + + var initResp struct { + ID string `json:"id"` + Status string `json:"status"` + } + if err := json.Unmarshal(respBody, &initResp); err != nil { + return nil, nil, fmt.Errorf("parse response: %w", err) + } + + if initResp.ID == "" { + return nil, nil, fmt.Errorf("no task id in BytePlus video response: %s", truncateBytes(respBody, 300)) + } + + return bytePlusVideoPollTask(ctx, apiKey, apiBase, initResp.ID, client) +} + +// bytePlusVideoPollTask polls GET /text-to-video-pro/status/{id} until done. +// Video gen typically takes 45s-3min. Allow up to ~5 minutes (300 polls × 1s). +func bytePlusVideoPollTask(ctx context.Context, apiKey, apiBase, taskID string, client *http.Client) ([]byte, *providers.Usage, error) { + statusURL := bytePlusVideoStatusEndpoint(apiBase, taskID) + slog.Info("create_video: BytePlus Seedance task started, polling", "task_id", taskID) + + const maxPolls = 300 + const pollInterval = 1 * time.Second + + for i := range maxPolls { + select { + case <-ctx.Done(): + return nil, nil, ctx.Err() + case <-time.After(pollInterval): + } + + pollReq, err := http.NewRequestWithContext(ctx, "GET", statusURL, nil) + if err != nil { + return nil, nil, fmt.Errorf("create poll request: %w", err) + } + pollReq.Header.Set("Authorization", "Bearer "+apiKey) + + pollResp, err := client.Do(pollReq) + if err != nil { + slog.Warn("create_video: BytePlus poll error, retrying", "error", err, "attempt", i+1) + continue + } + + respBytes, _ := io.ReadAll(pollResp.Body) + pollResp.Body.Close() + + if pollResp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("poll API error %d: %s", pollResp.StatusCode, truncateBytes(respBytes, 500)) + } + + var taskResp struct { + ID string `json:"id"` + Status string `json:"status"` + Output *struct { + VideoURL string `json:"video_url"` + } `json:"output"` + } + if err := json.Unmarshal(respBytes, &taskResp); err != nil { + return nil, nil, fmt.Errorf("parse poll response: %w", err) + } + + switch taskResp.Status { + case "succeeded": + if taskResp.Output == nil || taskResp.Output.VideoURL == "" { + return nil, nil, fmt.Errorf("task succeeded but no video URL") + } + return bytePlusDownloadVideo(ctx, taskResp.Output.VideoURL) + case "failed": + return nil, nil, fmt.Errorf("BytePlus video task %s failed", taskID) + default: + // Log progress every 10 polls to avoid spam + if (i+1)%10 == 0 { + slog.Info("create_video: BytePlus task pending", "attempt", i+1, "status", taskResp.Status) + } + } + } + + return nil, nil, fmt.Errorf("BytePlus video task %s timed out after %d polls", taskID, maxPolls) +} + +// bytePlusDownloadVideo downloads a video from a URL and returns the raw bytes. +func bytePlusDownloadVideo(ctx context.Context, videoURL string) ([]byte, *providers.Usage, error) { + req, err := http.NewRequestWithContext(ctx, "GET", videoURL, nil) + if err != nil { + return nil, nil, fmt.Errorf("create download request: %w", err) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("download video: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, nil, fmt.Errorf("download error %d: %s", resp.StatusCode, truncateBytes(body, 300)) + } + + videoBytes, err := limitedReadAll(resp.Body, maxMediaDownloadBytes) + if err != nil { + return nil, nil, fmt.Errorf("read video data: %w", err) + } + + return videoBytes, nil, nil +} diff --git a/internal/tools/media_provider_chain.go b/internal/tools/media_provider_chain.go index c706af19..a37c54c8 100644 --- a/internal/tools/media_provider_chain.go +++ b/internal/tools/media_provider_chain.go @@ -305,6 +305,8 @@ var dbTypeToMediaType = map[string]string{ "bailian": "dashscope", "anthropic_native": "anthropic", "suno": "suno", + "byteplus": "byteplus", + "byteplus_coding": "byteplus", } // ResolveProviderType returns the media routing type for a provider. @@ -345,6 +347,8 @@ func providerTypeFromName(name string) string { return "anthropic" case name == "suno" || strings.HasPrefix(name, "suno"): return "suno" + case name == "byteplus" || strings.HasPrefix(name, "byteplus"): + return "byteplus" case name == "yescale": return "openai" default: diff --git a/ui/desktop/frontend/src/constants/providers.ts b/ui/desktop/frontend/src/constants/providers.ts index b6c693dc..e74cb04c 100644 --- a/ui/desktop/frontend/src/constants/providers.ts +++ b/ui/desktop/frontend/src/constants/providers.ts @@ -23,6 +23,8 @@ export const PROVIDER_TYPES: ProviderTypeInfo[] = [ { value: 'yescale', label: 'YesScale', apiBase: 'https://api.yescale.one/v1', needsKey: true }, { value: 'zai', label: 'Z.ai API', apiBase: 'https://api.z.ai/api/paas/v4', needsKey: true }, { value: 'zai_coding', label: 'Z.ai Coding Plan', apiBase: 'https://api.z.ai/api/coding/paas/v4', needsKey: true }, + { value: 'byteplus', label: 'BytePlus ModelArk', apiBase: 'https://ark.ap-southeast.bytepluses.com/api/v3', needsKey: true }, + { value: 'byteplus_coding', label: 'BytePlus Coding Plan', apiBase: 'https://ark.ap-southeast.bytepluses.com/api/coding/v3', needsKey: true }, { value: 'ollama', label: 'Ollama (Local)', apiBase: 'http://localhost:11434/v1', needsKey: false }, { value: 'ollama_cloud', label: 'Ollama Cloud', apiBase: 'https://ollama.com/v1', needsKey: true }, { value: 'claude_cli', label: 'Claude CLI (Local)', apiBase: '', needsKey: false }, diff --git a/ui/web/src/constants/providers.ts b/ui/web/src/constants/providers.ts index b751dcda..34be5309 100644 --- a/ui/web/src/constants/providers.ts +++ b/ui/web/src/constants/providers.ts @@ -30,6 +30,8 @@ export const PROVIDER_TYPES: ProviderTypeInfo[] = [ { value: "yescale", label: "YesScale", apiBase: "https://api.yescale.one/v1", placeholder: "" }, { value: "zai", label: "Z.ai API", apiBase: "https://api.z.ai/api/paas/v4", placeholder: "" }, { value: "zai_coding", label: "Z.ai Coding Plan", apiBase: "https://api.z.ai/api/coding/paas/v4", placeholder: "" }, + { value: "byteplus", label: "BytePlus ModelArk", apiBase: "https://ark.ap-southeast.bytepluses.com/api/v3", placeholder: "" }, + { value: "byteplus_coding", label: "BytePlus Coding Plan", apiBase: "https://ark.ap-southeast.bytepluses.com/api/coding/v3", placeholder: "" }, { value: "ollama", label: "Ollama (Local)", apiBase: "http://localhost:11434/v1", placeholder: "" }, { value: "ollama_cloud", label: "Ollama Cloud", apiBase: "https://ollama.com/v1", placeholder: "" }, { value: "claude_cli", label: "Claude CLI (Local)", apiBase: "", placeholder: "" },