From 0c4d7176ffb215dbe3a77f9d1330714e3bc5a6a1 Mon Sep 17 00:00:00 2001 From: coohu <1257817341@qq.com> Date: Thu, 29 Jan 2026 13:46:16 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20ShengSuanYun=20(=E8=83=9C?= =?UTF-8?q?=E7=AE=97=E4=BA=91)=20as=20a=20model=20provider.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + docs/providers/shengsuanyun.md | 260 +++++++++++++++++ src/agents/model-auth.ts | 1 + ...dels-config.providers.shengsuanyun.test.ts | 15 + src/agents/models-config.providers.ts | 18 ++ src/agents/shengsuanyun-models.test.ts | 30 ++ src/agents/shengsuanyun-models.ts | 270 ++++++++++++++++++ src/commands/auth-choice-options.ts | 10 +- .../auth-choice.apply.api-providers.ts | 74 +++++ src/commands/model-picker.ts | 14 +- src/commands/onboard-auth.config-core.ts | 72 +++++ src/commands/onboard-auth.credentials.ts | 14 + src/commands/onboard-auth.ts | 2 + src/commands/onboard-types.ts | 2 + 14 files changed, 778 insertions(+), 5 deletions(-) create mode 100644 docs/providers/shengsuanyun.md create mode 100644 src/agents/models-config.providers.shengsuanyun.test.ts create mode 100644 src/agents/shengsuanyun-models.test.ts create mode 100644 src/agents/shengsuanyun-models.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e16c962a4..06c99efe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Status: beta. ### Changes - Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope. +- Models: add ShengSuanYun (胜算云) as a model provider with dynamic model discovery for both LLM and multimodal models (text-to-image, image-to-video, etc.). - Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. - macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). - macOS: finish Moltbot app rename for macOS sources, bundle identifiers, and shared kit paths. (#2844) Thanks @fal3. diff --git a/docs/providers/shengsuanyun.md b/docs/providers/shengsuanyun.md new file mode 100644 index 000000000..a6a8528cd --- /dev/null +++ b/docs/providers/shengsuanyun.md @@ -0,0 +1,260 @@ +--- +summary: "Use ShengSuanYun (胜算云) models in Moltbot" +read_when: + - You want to use ShengSuanYun model router + - You need ShengSuanYun setup guidance +--- +# ShengSuanYun (胜算云) + +ShengSuanYun provides a unified router for accessing multiple AI model providers through a single API endpoint, supporting both LLM models and multimodal generative models (text-to-image, image-to-video, etc.). + +## Why ShengSuanYun in Moltbot + +- **Unified API** for multiple model providers +- **LLM Support**: OpenAI, Anthropic, Google, DeepSeek, and many others +- **Multimodal Support**: Text-to-image, image-to-video, and other generative models +- **OpenAI-compatible** `/v1` endpoints for LLMs +- **Anthropic-compatible** `/v1/messages` endpoint +- **Wide model selection** from different providers +- **Automatic model discovery** from the provider's API + +## Features + +### LLM Models +- **Multi-provider access**: Access models from OpenAI, Anthropic, Google, Ali, ByteDance, DeepSeek, Meta, and more +- **Multiple API formats**: Supports `/v1/chat/completions`, `/v1/messages`, and `/v1/responses` +- **Streaming**: ✅ Supported on all compatible models +- **Function calling**: ✅ Supported on compatible models +- **Vision**: ✅ Supported on models with vision capability +- **Dynamic model discovery**: Models are automatically discovered from the API + +### Multimodal Models +- **Text-to-Image**: GPT-Image, Doubao-Seedream, Qwen-Image-Plus, Flux models +- **Text-to-Video**: Veo3.1, Sora2, 通义万相 (Wanxiang) models +- **Image-to-Video**: Doubao-Seedance, Wanxiang image-to-video models +- **Image-to-Image**: Flux-kontext-pro, Wanxiang image editing models +- **Automatic discovery**: Over 200+ multimodal models available + +## Setup + +### 1. Get API Key + +1. Sign up at [ShengSuanYun](https://shengsuanyun.com) +2. Navigate to [API settings](https://console.shengsuanyun.com/user/keys) +3. Generate an API key + +### 2. Configure Moltbot + +**Option A: Environment Variable** + +```bash +export SHENGSUANYUN_API_KEY="your-api-key" +``` + +**Option B: Config File** + +Add to your `moltbot.json`: + +```json5 +{ + env: { SHENGSUANYUN_API_KEY: "your-api-key" }, + agents: { + defaults: { + model: { primary: "shengsuanyun/anthropic/claude-opus-4.5" } + } + } +} +``` + +### 3. Verify Setup + +```bash +moltbot models list | grep shengsuanyun +moltbot chat --model shengsuanyun/anthropic/claude-opus-4.5 "Hello, are you working?" +``` + +## Model Selection + +ShengSuanYun provides access to hundreds of models from various providers. Models are identified by their provider prefix: + +### LLM Providers + +- **OpenAI**: `openai/gpt-5.1`, `openai/gpt-5.2`, `openai/o3` +- **Anthropic**: `anthropic/claude-opus-4.5`, `anthropic/claude-sonnet-4.5`, `anthropic/claude-haiku-4.5` +- **Google**: `google/gemini-3-pro-preview`, `google/gemini-3-flash` +- **DeepSeek**: `deepseek/deepseek-chat`, `deepseek/deepseek-reasoner` +- **Ali**: Various Qwen models +- **ByteDance**: Various Doubao models +- **Meta**: Llama models +- And many more... + +### Multimodal Models + +Multimodal models use the prefix `modality/{id}` format: + +#### Text-to-Image Models +- **GPT-Image**: OpenAI's image generation models +- **Doubao-Seedream**: ByteDance's text-to-image models (4.5 series) +- **Qwen-Image-Plus**: Ali's advanced image generation +- **Flux**: BlackForestLabs' high-quality image models + +#### Text-to-Video Models +- **Veo3.1**: Google's video generation model +- **Sora2**: OpenAI's video generation model +- **通义万相 (Wanxiang)**: Ali's text-to-video models (2.2-Plus) + +#### Image-to-Video Models +- **Doubao-Seedance**: ByteDance's image-to-video conversion +- **通义万相 (Wanxiang)**: Ali's image-to-video models (2.5, 2.6) + +#### Image-to-Image Models +- **Flux-kontext-pro**: Advanced image editing +- **通义万相 (Wanxiang)**: Ali's image editing models (2.5) + +List all available models: + +```bash +# List all models +moltbot models list | grep shengsuanyun + +# List only LLM models +moltbot models list | grep "shengsuanyun" | grep -v "modality" + +# List only multimodal models +moltbot models list | grep "shengsuanyun/modality" +``` + +Change your default model: + +```bash +# Set LLM model +moltbot models set shengsuanyun/anthropic/claude-opus-4.5 + +# Set multimodal model (if supported by your workflow) +moltbot models set shengsuanyun/modality/256 +``` + +## Model Discovery + +Moltbot automatically discovers models from two ShengSuanYun APIs when `SHENGSUANYUN_API_KEY` is configured: + +1. **LLM Models API**: `https://router.shengsuanyun.com/api/v1/models` + - Returns all text-based chat and completion models + - Includes models from major AI providers + - Supports filtering by API compatibility + +2. **Multimodal Models API**: `https://api.shengsuanyun.com/modelrouter/modalities/list` + - Returns generative models for images and videos + - Includes text-to-image, image-to-video, and image-to-image models + - Over 200+ models available + +Each model includes: +- Model ID and name +- Company/provider information +- Context window size and max tokens (for LLMs) +- Maximum output tokens +- Supported APIs +- Pricing information +- Input modality support (text, image, etc.) +- Model capabilities and classifications + +## API Compatibility + +ShengSuanYun supports multiple API formats: + +| API Format | Endpoint | Compatible With | +|------------|----------|-----------------| +| OpenAI Completions | `/v1/chat/completions` | OpenAI SDK | +| Anthropic Messages | `/v1/messages` | Claude SDK | +| OpenAI Responses | `/v1/responses` | OpenAI SDK | + +Moltbot automatically uses the appropriate API format based on the model's capabilities, preferring the OpenAI completions format when available. + +## Usage Examples + +### LLM Models + +```bash +# Use Claude via ShengSuanYun +moltbot chat --model shengsuanyun/anthropic/claude-opus-4.5 + +# Use GPT-5.2 +moltbot chat --model shengsuanyun/openai/gpt-5.2 + +# Use Gemini +moltbot chat --model shengsuanyun/google/gemini-3-pro-preview + +# Use DeepSeek +moltbot chat --model shengsuanyun/deepseek/deepseek-chat +``` + +### Multimodal Models + +Note: Multimodal model integration depends on your specific workflow and use case. The models are discovered and listed but may require additional configuration or API integration for image/video generation tasks. + +```bash +# List available multimodal models +moltbot models list | grep "modality" + +# Example multimodal model IDs (text-to-image, image-to-video, etc.) +# - shengsuanyun/modality/256 (Ali Wanxiang 2.6 I2V) +# - shengsuanyun/modality/XXX (Other generative models) +``` + +## Configuration Example + +Full configuration in `moltbot.json`: + +```json5 +{ + env: { SHENGSUANYUN_API_KEY: "your-api-key" }, + agents: { + defaults: { + model: { primary: "shengsuanyun/anthropic/claude-opus-4.5" } + } + }, + models: { + mode: "merge", + providers: { + shengsuanyun: { + baseUrl: "https://router.shengsuanyun.com/api/v1", + apiKey: "${SHENGSUANYUN_API_KEY}", + api: "openai-completions", + models: [] // Models are auto-discovered + } + } + } +} +``` + +## Pricing + +ShengSuanYun uses its own pricing model. Check the ShengSuanYun dashboard for current rates per model. Pricing varies by: +- Model provider +- Model size and capability +- Input/output tokens +- Additional features (vision, etc.) + +## Troubleshooting + +### API key not recognized + +```bash +echo $SHENGSUANYUN_API_KEY +moltbot models list | grep shengsuanyun +``` + +Verify your API key is valid and has the correct permissions. + +### Model not available + +The ShengSuanYun model catalog updates dynamically. Run `moltbot models list` to see currently available models. Some models may be temporarily unavailable. + +### Connection issues + +ShengSuanYun API is at `https://router.shengsuanyun.com/api/v1`. Ensure your network allows HTTPS connections. + +## Links + +- [ShengSuanYun Website](https://router.shengsuanyun.com) +- [Model List API](https://router.shengsuanyun.com/api/v1/models) diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 96e4e4ae6..1a17b1ce0 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -285,6 +285,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { venice: "VENICE_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", + shengsuanyun: "SHENGSUANYUN_API_KEY", }; const envVar = envMap[normalized]; if (!envVar) return null; diff --git a/src/agents/models-config.providers.shengsuanyun.test.ts b/src/agents/models-config.providers.shengsuanyun.test.ts new file mode 100644 index 000000000..997b47951 --- /dev/null +++ b/src/agents/models-config.providers.shengsuanyun.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { resolveImplicitProviders } from "./models-config.providers.js"; +import { mkdtempSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("ShengSuanYun provider", () => { + it("should not include shengsuanyun when no API key is configured", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "clawd-test-")); + const providers = await resolveImplicitProviders({ agentDir }); + + // ShengSuanYun requires explicit configuration via SHENGSUANYUN_API_KEY env var or profile + expect(providers?.shengsuanyun).toBeUndefined(); + }); +}); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index a176dac8a..525134dde 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -13,6 +13,7 @@ import { SYNTHETIC_MODEL_CATALOG, } from "./synthetic-models.js"; import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js"; +import { discoverAllShengSuanYunModels, SHENGSUANYUN_BASE_URL } from "./shengsuanyun-models.js"; type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; @@ -359,6 +360,15 @@ async function buildOllamaProvider(): Promise { }; } +async function buildShengSuanYunProvider(): Promise { + const models = await discoverAllShengSuanYunModels(); + return { + baseUrl: SHENGSUANYUN_BASE_URL, + api: "openai-completions", + models, + }; +} + export async function resolveImplicitProviders(params: { agentDir: string; }): Promise { @@ -418,6 +428,14 @@ export async function resolveImplicitProviders(params: { providers.ollama = { ...(await buildOllamaProvider()), apiKey: ollamaKey }; } + // ShengSuanYun provider - only add if explicitly configured + const shengsuanyunKey = + resolveEnvApiKeyVarName("shengsuanyun") ?? + resolveApiKeyFromProfiles({ provider: "shengsuanyun", store: authStore }); + if (shengsuanyunKey) { + providers.shengsuanyun = { ...(await buildShengSuanYunProvider()), apiKey: shengsuanyunKey }; + } + return providers; } diff --git a/src/agents/shengsuanyun-models.test.ts b/src/agents/shengsuanyun-models.test.ts new file mode 100644 index 000000000..0a1646da4 --- /dev/null +++ b/src/agents/shengsuanyun-models.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { + discoverShengSuanYunModels, + discoverShengSuanYunModalityModels, + discoverAllShengSuanYunModels, + SHENGSUANYUN_BASE_URL, + SHENGSUANYUN_MODALITIES_BASE_URL, +} from "./shengsuanyun-models.js"; + +describe("ShengSuanYun provider", () => { + it("should have the correct base URLs", () => { + expect(SHENGSUANYUN_BASE_URL).toBe("https://router.shengsuanyun.com/api/v1"); + expect(SHENGSUANYUN_MODALITIES_BASE_URL).toBe("https://api.shengsuanyun.com/modelrouter"); + }); + + it("should skip LLM discovery in test environment", async () => { + const models = await discoverShengSuanYunModels(); + expect(models).toEqual([]); + }); + + it("should skip multimodal discovery in test environment", async () => { + const models = await discoverShengSuanYunModalityModels(); + expect(models).toEqual([]); + }); + + it("should skip all model discovery in test environment", async () => { + const models = await discoverAllShengSuanYunModels(); + expect(models).toEqual([]); + }); +}); diff --git a/src/agents/shengsuanyun-models.ts b/src/agents/shengsuanyun-models.ts new file mode 100644 index 000000000..dd8ba80fb --- /dev/null +++ b/src/agents/shengsuanyun-models.ts @@ -0,0 +1,270 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +export const SHENGSUANYUN_BASE_URL = "https://router.shengsuanyun.com/api/v1"; +export const SHENGSUANYUN_MODALITIES_BASE_URL = "https://api.shengsuanyun.com/modelrouter"; + +// ShengSuanYun uses credit-based pricing. Set to 0 as costs vary by model. +export const SHENGSUANYUN_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +// ShengSuanYun API response types for LLM models +interface ShengSuanYunModel { + id: string; + company: string; + name: string; + api_name: string; + description: string; + max_tokens: number; + context_window: number; + supports_prompt_cache: boolean; + architecture: { + modality: string; + tokenizer: string; + instruct_type: string | null; + }; + pricing: { + prompt: string; + completion: string; + request: string; + image?: string; + tts?: string; + }; + support_apis: string[]; +} + +interface ShengSuanYunModelsResponse { + data: ShengSuanYunModel[]; + object: string; + success: boolean; +} + +// ShengSuanYun multimodal API response types +interface ShengSuanYunModalityModel { + id: number; + model_name: string; + company_name: string; + class_name: string; + class_names: string[]; + desc: string; + preview_img: string; + preview_video?: string; + usage: number; + pricing: { + input_price: number; + output_price: number; + currency: string; + }; +} + +interface ShengSuanYunModalitiesResponse { + code: number; + data: { + infos: ShengSuanYunModalityModel[]; + }; +} + +/** + * Determine if a model supports reasoning based on its name and description. + */ +function isReasoningModel(model: ShengSuanYunModel): boolean { + const lowerName = (model.name ?? "").toLowerCase(); + const lowerId = (model.id ?? "").toLowerCase(); + const lowerDesc = (model.description ?? "").toLowerCase(); + + return ( + lowerName.includes("thinking") || + lowerName.includes("reasoning") || + lowerName.includes("reason") || + lowerName.includes("r1") || + lowerId.includes("thinking") || + lowerId.includes("reasoning") || + lowerId.includes("r1") || + lowerDesc.includes("reasoning") || + lowerDesc.includes("thinking") + ); +} + +/** + * Determine if a model supports vision/image inputs. + */ +function supportsVision(model: ShengSuanYunModel): boolean { + const modality = (model.architecture?.modality ?? "").toLowerCase(); + return ( + modality.includes("image") || modality.includes("vision") || modality === "text+image->text" + ); +} + +/** + * Build a ModelDefinitionConfig from a ShengSuanYun API model. + */ +function buildShengSuanYunModelDefinition(model: ShengSuanYunModel): ModelDefinitionConfig { + const hasVision = supportsVision(model); + const reasoning = isReasoningModel(model); + + return { + id: model.id, + name: model.name, + reasoning, + input: hasVision ? ["text", "image"] : ["text"], + cost: SHENGSUANYUN_DEFAULT_COST, + contextWindow: model.context_window || 128000, + maxTokens: model.max_tokens || 8192, + }; +} + +/** + * Discover models from ShengSuanYun API. + * The /models endpoint is public and doesn't require authentication. + */ +export async function discoverShengSuanYunModels(): Promise { + // Skip API discovery in test environment + if (process.env.NODE_ENV === "test" || process.env.VITEST) { + return []; + } + + try { + const response = await fetch(`${SHENGSUANYUN_BASE_URL}/models`, { + signal: AbortSignal.timeout(10000), // 10s timeout for large model list + }); + + if (!response.ok) { + // console.warn( + // `[shengsuanyun-models] Failed to discover models: HTTP ${response.status}`, + // ); + return []; + } + + const data = (await response.json()) as ShengSuanYunModelsResponse; + + if (!data.success || !Array.isArray(data.data) || data.data.length === 0) { + // console.warn("[shengsuanyun-models] No models found from API"); + return []; + } + + const models: ModelDefinitionConfig[] = []; + for (const apiModel of data.data) { + // Only include models that support at least one compatible API + const supportApis = apiModel.support_apis; + if (!Array.isArray(supportApis)) { + continue; + } + + const hasCompatibleApi = supportApis.some( + (api) => + api === "/v1/chat/completions" || api === "/v1/messages" || api === "/v1/responses", + ); + + if (!hasCompatibleApi) { + continue; + } + + models.push(buildShengSuanYunModelDefinition(apiModel)); + } + + // console.log(`[shengsuanyun-models] Discovered ${models.length} LLM models`); + return models; + } catch (error) { + // console.warn(`[shengsuanyun-models] Discovery failed: ${String(error)}`); + return []; + } +} + +/** + * Determine modality input types from class names. + */ +function getModalityInputTypes(classNames: string[]): Array<"text" | "image"> { + if (!Array.isArray(classNames)) return ["text"]; + + const hasText = classNames.some((name) => name && (name.includes("text") || name.includes("文"))); + const hasImage = classNames.some( + (name) => + name && + (name.includes("image") || + name.includes("图") || + name.includes("video") || + name.includes("视频")), + ); + + const inputs: Array<"text" | "image"> = []; + if (hasText) inputs.push("text"); + if (hasImage) inputs.push("image"); + + // Default to text if no clear input type + return inputs.length > 0 ? inputs : ["text"]; +} + +function buildShengSuanYunModalityModelDefinition( + model: ShengSuanYunModalityModel, +): ModelDefinitionConfig { + const inputs = getModalityInputTypes(model.class_names); + + return { + id: `modality/${model.id}`, + name: `${model.model_name} (${model.company_name})`, + reasoning: false, // Multimodal models typically don't do reasoning + input: inputs, + cost: SHENGSUANYUN_DEFAULT_COST, + contextWindow: 128000, // Default context window for multimodal models + maxTokens: 8192, + }; +} + +export async function discoverShengSuanYunModalityModels(): Promise { + // Skip API discovery in test environment + if (process.env.NODE_ENV === "test" || process.env.VITEST) { + return []; + } + + try { + const response = await fetch( + `${SHENGSUANYUN_MODALITIES_BASE_URL}/modalities/list?page=1&page_size=200`, + { + signal: AbortSignal.timeout(10000), // 10s timeout + }, + ); + + if (!response.ok) { + // console.warn( + // `[shengsuanyun-modalities] Failed to discover modality models: HTTP ${response.status}`, + // ); + return []; + } + + const data = (await response.json()) as ShengSuanYunModalitiesResponse; + + if (data.code !== 0 || !Array.isArray(data.data.infos) || data.data.infos.length === 0) { + // console.warn("[shengsuanyun-modalities] No modality models found from API"); + return []; + } + + const models: ModelDefinitionConfig[] = data.data.infos.map( + buildShengSuanYunModalityModelDefinition, + ); + + // console.log(`[shengsuanyun-modalities] Discovered ${models.length} modality models`); + return models; + } catch (error) { + // console.warn(`[shengsuanyun-modalities] Discovery failed: ${String(error)}`); + return []; + } +} + +/** + * Discover all ShengSuanYun models (LLM + multimodal). + */ +export async function discoverAllShengSuanYunModels(): Promise { + const [llmModels, modalityModels] = await Promise.all([ + discoverShengSuanYunModels(), + discoverShengSuanYunModalityModels(), + ]); + + const allModels = [...llmModels, ...modalityModels]; + // console.log( + // `[shengsuanyun] Discovered ${allModels.length} total models (${llmModels.length} LLM, ${modalityModels.length} multimodal)` + // ); + return allModels; +} diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..84c68a12a 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -20,7 +20,8 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" - | "qwen"; + | "qwen" + | "shengsuanyun"; export type AuthChoiceGroup = { value: AuthChoiceGroupId; @@ -113,6 +114,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["opencode-zen"], }, + { + value: "shengsuanyun", + label: "ShengSuanYun", + hint: "API key", + choices: ["shengsuanyun-api-key"], + }, ]; export function buildAuthChoiceOptions(params: { @@ -142,6 +149,7 @@ export function buildAuthChoiceOptions(params: { options.push({ value: "moonshot-api-key", label: "Moonshot AI API key" }); options.push({ value: "kimi-code-api-key", label: "Kimi Code API key" }); options.push({ value: "synthetic-api-key", label: "Synthetic API key" }); + options.push({ value: "shengsuanyun-api-key", label: "ShengSuanYun API key" }); options.push({ value: "venice-api-key", label: "Venice AI API key", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..119efd74c 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -11,6 +11,10 @@ import { applyGoogleGeminiModelDefault, GOOGLE_GEMINI_DEFAULT_MODEL, } from "./google-gemini-model-default.js"; +import { + setShengSuanYunApiKey, + SHENGSUANYUN_DEFAULT_MODEL_REF, +} from "./onboard-auth.credentials.js"; import { applyAuthProfileConfig, applyKimiCodeConfig, @@ -21,6 +25,8 @@ import { applyOpencodeZenProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, + applyShengSuanYunConfig, + applyShengSuanYunProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -579,5 +585,73 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "shengsuanyun-api-key") { + const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }); + const profileOrder = resolveAuthProfileOrder({ + cfg: nextConfig, + store, + provider: "shengsuanyun", + }); + const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId])); + const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined; + let profileId = "shengsuanyun:default"; + let mode: "api_key" | "token" = "api_key"; + let hasCredential = false; + + if (existingProfileId && existingCred?.type) { + profileId = existingProfileId; + mode = existingCred.type === "token" ? "token" : "api_key"; + hasCredential = true; + } + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "shengsuanyun") { + await setShengSuanYunApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + const envKey = resolveEnvApiKey("shengsuanyun"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing SHENGSUANYUN_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setShengSuanYunApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + } + + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter ShengSuanYun API key", + validate: validateApiKeyInput, + }); + await setShengSuanYunApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + hasCredential = true; + } + + if (hasCredential) { + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId, + provider: "shengsuanyun", + mode, + }); + } + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: SHENGSUANYUN_DEFAULT_MODEL_REF, + applyDefaultConfig: applyShengSuanYunConfig, + applyProviderConfig: applyShengSuanYunProviderConfig, + noteDefault: SHENGSUANYUN_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + return { config: applied.config, agentModelOverride }; + } + return null; } diff --git a/src/commands/model-picker.ts b/src/commands/model-picker.ts index f2283fe8a..731861d2d 100644 --- a/src/commands/model-picker.ts +++ b/src/commands/model-picker.ts @@ -211,7 +211,10 @@ export async function promptDefaultModel( // Skip internal router models that can't be directly called via API. if (HIDDEN_ROUTER_MODELS.has(key)) return; const hints: string[] = []; - if (entry.name && entry.name !== entry.id) hints.push(entry.name); + // For models with a distinct name, show the name as label and key as hint + const hasDistinctName = entry.name && entry.name !== entry.id; + const label = hasDistinctName ? (entry.name ?? key) : key; + if (hasDistinctName) hints.push(key); if (entry.contextWindow) hints.push(`ctx ${formatTokenK(entry.contextWindow)}`); if (entry.reasoning) hints.push("reasoning"); const aliases = aliasIndex.byKey.get(key); @@ -219,7 +222,7 @@ export async function promptDefaultModel( if (!hasAuth(entry.provider)) hints.push("auth missing"); options.push({ value: key, - label: key, + label, hint: hints.length > 0 ? hints.join(" · ") : undefined, }); seen.add(key); @@ -338,7 +341,10 @@ export async function promptModelAllowlist(params: { if (seen.has(key)) return; if (HIDDEN_ROUTER_MODELS.has(key)) return; const hints: string[] = []; - if (entry.name && entry.name !== entry.id) hints.push(entry.name); + // For models with a distinct name, show the name as label and key as hint + const hasDistinctName = entry.name && entry.name !== entry.id; + const label = hasDistinctName ? (entry.name ?? key) : key; + if (hasDistinctName) hints.push(key); if (entry.contextWindow) hints.push(`ctx ${formatTokenK(entry.contextWindow)}`); if (entry.reasoning) hints.push("reasoning"); const aliases = aliasIndex.byKey.get(key); @@ -346,7 +352,7 @@ export async function promptModelAllowlist(params: { if (!hasAuth(entry.provider)) hints.push("auth missing"); options.push({ value: key, - label: key, + label, hint: hints.length > 0 ? hints.join(" · ") : undefined, }); seen.add(key); diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 921ee01d1..9eaeac6a1 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -10,9 +10,11 @@ import { VENICE_DEFAULT_MODEL_REF, VENICE_MODEL_CATALOG, } from "../agents/venice-models.js"; +import { SHENGSUANYUN_BASE_URL } from "../agents/shengsuanyun-models.js"; import type { MoltbotConfig } from "../config/config.js"; import { OPENROUTER_DEFAULT_MODEL_REF, + SHENGSUANYUN_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; @@ -411,6 +413,76 @@ export function applyVeniceConfig(cfg: MoltbotConfig): MoltbotConfig { }; } +/** + * Apply ShengSuanYun provider configuration only (adds to models.providers). + */ +export function applyShengSuanYunProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[SHENGSUANYUN_DEFAULT_MODEL_REF] = { + ...models[SHENGSUANYUN_DEFAULT_MODEL_REF], + alias: models[SHENGSUANYUN_DEFAULT_MODEL_REF]?.alias ?? "ShengSuanYun", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.shengsuanyun; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + + providers.shengsuanyun = { + ...existingProviderRest, + baseUrl: SHENGSUANYUN_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + // Models will be discovered automatically by resolveImplicitProviders + models: [], + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply ShengSuanYun provider configuration AND set ShengSuanYun as the default model. + * Use this when ShengSuanYun is the primary provider choice during onboarding. + */ +export function applyShengSuanYunConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyShengSuanYunProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: SHENGSUANYUN_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: MoltbotConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index b2fb58542..c08806cbe 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -164,3 +164,17 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export const SHENGSUANYUN_DEFAULT_MODEL_REF = "shengsuanyun/openai/gpt-5-nano"; +export async function setShengSuanYunApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "shengsuanyun:default", + credential: { + type: "api_key", + provider: "shengsuanyun", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..ce48d8b27 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -11,6 +11,8 @@ export { applyMoonshotProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, + applyShengSuanYunConfig, + applyShengSuanYunProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..b3383c172 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -31,6 +31,7 @@ export type AuthChoice = | "github-copilot" | "copilot-proxy" | "qwen-portal" + | "shengsuanyun-api-key" | "skip"; export type GatewayAuthChoice = "token" | "password"; export type ResetScope = "config" | "config+creds+sessions" | "full"; @@ -71,6 +72,7 @@ export type OnboardOptions = { syntheticApiKey?: string; veniceApiKey?: string; opencodeZenApiKey?: string; + shengSuanYunApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind; gatewayAuth?: GatewayAuthChoice; From e597c6d398be59176b38b631e683eaffcc841f0c Mon Sep 17 00:00:00 2001 From: coohu <1257817341@qq.com> Date: Thu, 29 Jan 2026 14:28:43 +0800 Subject: [PATCH 2/2] fix: lint error. --- src/agents/shengsuanyun-models.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agents/shengsuanyun-models.ts b/src/agents/shengsuanyun-models.ts index dd8ba80fb..11f4a7602 100644 --- a/src/agents/shengsuanyun-models.ts +++ b/src/agents/shengsuanyun-models.ts @@ -167,7 +167,7 @@ export async function discoverShengSuanYunModels(): Promise