From 5d3f4940da3b7022f16e8c3eea48de3d3f6db31a Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Wed, 17 Jun 2026 15:46:59 +0800 Subject: [PATCH 1/4] feat: add iFlytek Spark and Astron MaaS providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add declarative OpenAI-compatible providers for iFlytek Spark (讯飞星火) and iFlytek Astron MaaS (讯飞星辰): - iflytek.json: Spark HTTP API (spark-api-open.xf-yun.com/v1) with 4.0Ultra, generalv3.5, max-32k, generalv3, pro-128k and lite, using SPARK_API_PASSWORD for auth. - iflytek_astron.json: Astron MaaS, defaulting to the Token Plan endpoint with ASTRON_BASE_URL to switch to the Coding Plan; ships Spark X2, DeepSeek, GLM, Kimi, MiniMax, Qwen and astron-code-latest. Both use static model lists and skip canonical filtering since the model ids are provider-specific. Adds deserialization tests and documents the providers in the supported providers table. Signed-off-by: FenjuFu --- .../goose/src/config/declarative_providers.rs | 44 +++++++++++++++++++ .../src/providers/declarative/iflytek.json | 25 +++++++++++ .../providers/declarative/iflytek_astron.json | 41 +++++++++++++++++ .../docs/getting-started/providers.md | 2 + 4 files changed, 112 insertions(+) create mode 100644 crates/goose/src/providers/declarative/iflytek.json create mode 100644 crates/goose/src/providers/declarative/iflytek_astron.json diff --git a/crates/goose/src/config/declarative_providers.rs b/crates/goose/src/config/declarative_providers.rs index a3a8fdc10475..abfb40ce65ac 100644 --- a/crates/goose/src/config/declarative_providers.rs +++ b/crates/goose/src/config/declarative_providers.rs @@ -832,6 +832,50 @@ mod tests { ); } + #[test] + fn test_iflytek_json_deserializes() { + let json = include_str!("../providers/declarative/iflytek.json"); + let config: DeclarativeProviderConfig = + serde_json::from_str(json).expect("iflytek.json should parse"); + assert_eq!(config.name, "iflytek"); + assert_eq!(config.display_name, "iFlytek Spark"); + assert!(matches!(config.engine, ProviderEngine::OpenAI)); + assert_eq!(config.api_key_env, "SPARK_API_PASSWORD"); + assert_eq!(config.base_url, "https://spark-api-open.xf-yun.com/v1"); + assert_eq!(config.dynamic_models, Some(false)); + assert!(config.skip_canonical_filtering); + assert_eq!(config.supports_streaming, Some(true)); + assert!(config.models.iter().any(|m| m.name == "4.0Ultra")); + assert!(config.models.iter().any(|m| m.name == "lite")); + } + + #[test] + fn test_iflytek_astron_json_deserializes() { + let json = include_str!("../providers/declarative/iflytek_astron.json"); + let config: DeclarativeProviderConfig = + serde_json::from_str(json).expect("iflytek_astron.json should parse"); + assert_eq!(config.name, "iflytek_astron"); + assert_eq!(config.display_name, "iFlytek Astron MaaS"); + assert!(matches!(config.engine, ProviderEngine::OpenAI)); + assert_eq!(config.api_key_env, "ASTRON_API_KEY"); + assert_eq!(config.base_url, "${ASTRON_BASE_URL}"); + assert_eq!(config.dynamic_models, Some(false)); + assert!(config.skip_canonical_filtering); + assert_eq!(config.supports_streaming, Some(true)); + + let env_vars = config.env_vars.as_ref().expect("env_vars should be set"); + assert_eq!(env_vars.len(), 1); + assert_eq!(env_vars[0].name, "ASTRON_BASE_URL"); + assert!(!env_vars[0].required); + assert!(!env_vars[0].secret); + assert_eq!( + env_vars[0].default, + Some("https://maas-token-api.cn-huabei-1.xf-yun.com/v2".to_string()) + ); + assert!(config.models.iter().any(|m| m.name == "xsparkx2")); + assert!(config.models.iter().any(|m| m.name == "astron-code-latest")); + } + #[test] fn test_existing_json_files_still_deserialize_without_new_fields() { let json = include_str!("../providers/declarative/groq.json"); diff --git a/crates/goose/src/providers/declarative/iflytek.json b/crates/goose/src/providers/declarative/iflytek.json new file mode 100644 index 000000000000..98a4cc62618e --- /dev/null +++ b/crates/goose/src/providers/declarative/iflytek.json @@ -0,0 +1,25 @@ +{ + "name": "iflytek", + "engine": "openai", + "display_name": "iFlytek Spark", + "description": "iFlytek Spark (讯飞星火) models via the OpenAI-compatible HTTP API. Authenticate with your Spark HTTP API password (APIPassword).", + "api_key_env": "SPARK_API_PASSWORD", + "base_url": "https://spark-api-open.xf-yun.com/v1", + "dynamic_models": false, + "skip_canonical_filtering": true, + "models": [ + {"name": "4.0Ultra", "context_limit": 8192}, + {"name": "generalv3.5", "context_limit": 8192}, + {"name": "max-32k", "context_limit": 32768}, + {"name": "generalv3", "context_limit": 8192}, + {"name": "pro-128k", "context_limit": 131072}, + {"name": "lite", "context_limit": 4096} + ], + "supports_streaming": true, + "model_doc_link": "https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html", + "setup_steps": [ + "Sign in to https://xinghuo.xfyun.cn/sparkapi and create or select a Spark model plan", + "Open the model's 'HTTP 服务接口认证信息' page and copy the APIPassword (each model version has its own password)", + "Paste the APIPassword above as SPARK_API_PASSWORD" + ] +} diff --git a/crates/goose/src/providers/declarative/iflytek_astron.json b/crates/goose/src/providers/declarative/iflytek_astron.json new file mode 100644 index 000000000000..a239644e77be --- /dev/null +++ b/crates/goose/src/providers/declarative/iflytek_astron.json @@ -0,0 +1,41 @@ +{ + "name": "iflytek_astron", + "engine": "openai", + "display_name": "iFlytek Astron MaaS", + "description": "iFlytek Astron MaaS (讯飞星辰) models via the OpenAI-compatible API. Defaults to the Token Plan endpoint; set ASTRON_BASE_URL to https://maas-coding-api.cn-huabei-1.xf-yun.com/v2 to use the Coding Plan (e.g. astron-code-latest). Each plan has its own API key.", + "api_key_env": "ASTRON_API_KEY", + "base_url": "${ASTRON_BASE_URL}", + "env_vars": [ + { + "name": "ASTRON_BASE_URL", + "required": false, + "secret": false, + "default": "https://maas-token-api.cn-huabei-1.xf-yun.com/v2", + "description": "Astron MaaS API base URL. Use https://maas-coding-api.cn-huabei-1.xf-yun.com/v2 for Coding Plan models like astron-code-latest." + } + ], + "dynamic_models": false, + "skip_canonical_filtering": true, + "models": [ + {"name": "xsparkx2", "context_limit": 131072}, + {"name": "xsparkx2flash", "context_limit": 131072}, + {"name": "xopglm51", "context_limit": 204800}, + {"name": "xopglm5", "context_limit": 204800}, + {"name": "xopdeepseekv4pro", "context_limit": 131072}, + {"name": "xopdeepseekv4flash", "context_limit": 131072}, + {"name": "xopdeepseekv32", "context_limit": 131072}, + {"name": "xopkimik26", "context_limit": 262144}, + {"name": "xminimaxm25", "context_limit": 204800}, + {"name": "xopqwen35397b", "context_limit": 262144}, + {"name": "astron-code-latest", "context_limit": 131072} + ], + "supports_streaming": true, + "preserves_thinking": true, + "model_doc_link": "https://www.xfyun.cn/doc/spark/TokenPlan.html", + "setup_steps": [ + "Sign in to https://maas.xfyun.cn and subscribe to a Token Plan (general models) or Coding Plan (astron-code-latest)", + "Copy the dedicated API Key for your plan from the subscription page", + "Paste the API Key above as ASTRON_API_KEY", + "For Coding Plan, set ASTRON_BASE_URL to https://maas-coding-api.cn-huabei-1.xf-yun.com/v2" + ] +} diff --git a/documentation/docs/getting-started/providers.md b/documentation/docs/getting-started/providers.md index edf36c9f734a..ec625b53c6a2 100644 --- a/documentation/docs/getting-started/providers.md +++ b/documentation/docs/getting-started/providers.md @@ -36,6 +36,8 @@ goose is compatible with a wide range of LLM providers, allowing you to choose a | [GCP Vertex AI](https://cloud.google.com/vertex-ai) | Google Cloud's Vertex AI platform, supporting Gemini and Claude models. **Credentials must be [configured in advance](https://cloud.google.com/vertex-ai/docs/authentication).** Filters for allowed models by organization policy (if configured). | `GCP_PROJECT_ID`, `GCP_LOCATION` and optionally `GCP_MAX_RATE_LIMIT_RETRIES` (5), `GCP_MAX_OVERLOADED_RETRIES` (5), `GCP_INITIAL_RETRY_INTERVAL_MS` (5000), `GCP_BACKOFF_MULTIPLIER` (2.0), `GCP_MAX_RETRY_INTERVAL_MS` (320_000). | | [GitHub Copilot](https://docs.github.com/en/copilot/using-github-copilot/ai-models) | Access to AI models from OpenAI, Anthropic, Google, and other providers through GitHub's Copilot infrastructure. **GitHub account with Copilot access required.** | No manual key. Uses [device flow authentication](#github-copilot-authentication) for both CLI and Desktop. | | [Groq](https://groq.com/) | High-performance inference hardware and tools for LLMs. | `GROQ_API_KEY` | +| [iFlytek Spark](https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html) | iFlytek Spark (讯飞星火) models via the OpenAI-compatible HTTP API, including 4.0Ultra, generalv3.5, and lite. | `SPARK_API_PASSWORD` | +| [iFlytek Astron MaaS](https://maas.xfyun.cn/) | iFlytek Astron MaaS (讯飞星辰) hosting Spark X2, DeepSeek, GLM, Kimi, MiniMax, Qwen, and Astron coding models via an OpenAI-compatible API. Set `ASTRON_BASE_URL` to switch between the Token Plan and Coding Plan endpoints. | `ASTRON_API_KEY`, `ASTRON_BASE_URL` (optional) | | [LiteLLM](https://docs.litellm.ai/docs/) | LiteLLM proxy supporting multiple models with automatic prompt caching and unified API access. | `LITELLM_HOST`, `LITELLM_BASE_PATH` (optional), `LITELLM_API_KEY` (optional), `LITELLM_CUSTOM_HEADERS` (optional), `LITELLM_TIMEOUT` (optional) | | [LM Studio](https://lmstudio.ai/) | Run local models with LM Studio's OpenAI-compatible server. **Because this provider runs locally, you must first [download a model](#local-llms).** | None required. Connects to local server at `localhost:1234` by default. | | [Mistral AI](https://mistral.ai/) | Provides access to Mistral models including general-purpose models, specialized coding models (Codestral), and multimodal models (Pixtral). | `MISTRAL_API_KEY` | From d03354d81b39b015c5bae0d1081f2bdb9a4c2eb5 Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Tue, 23 Jun 2026 20:45:05 +0800 Subject: [PATCH 2/4] fix(iflytek): limit Spark models to system/tool-capable versions; test all bundled providers parse Per the Spark HTTP docs only the 4.0Ultra and Max families accept system messages and Function Call, so drop the Pro/Pro-128K/Lite entries that would reject goose's system-prompt + tools requests. Replace the per-file iflytek deserialization tests with a single test that asserts every bundled declarative provider parses through the same path load_fixed_providers uses, which otherwise silently skips invalid files. --- .../goose/src/config/declarative_providers.rs | 61 +++++++------------ .../src/providers/declarative/iflytek.json | 7 +-- 2 files changed, 23 insertions(+), 45 deletions(-) diff --git a/crates/goose/src/config/declarative_providers.rs b/crates/goose/src/config/declarative_providers.rs index abfb40ce65ac..8df747d03b8b 100644 --- a/crates/goose/src/config/declarative_providers.rs +++ b/crates/goose/src/config/declarative_providers.rs @@ -833,47 +833,28 @@ mod tests { } #[test] - fn test_iflytek_json_deserializes() { - let json = include_str!("../providers/declarative/iflytek.json"); - let config: DeclarativeProviderConfig = - serde_json::from_str(json).expect("iflytek.json should parse"); - assert_eq!(config.name, "iflytek"); - assert_eq!(config.display_name, "iFlytek Spark"); - assert!(matches!(config.engine, ProviderEngine::OpenAI)); - assert_eq!(config.api_key_env, "SPARK_API_PASSWORD"); - assert_eq!(config.base_url, "https://spark-api-open.xf-yun.com/v1"); - assert_eq!(config.dynamic_models, Some(false)); - assert!(config.skip_canonical_filtering); - assert_eq!(config.supports_streaming, Some(true)); - assert!(config.models.iter().any(|m| m.name == "4.0Ultra")); - assert!(config.models.iter().any(|m| m.name == "lite")); - } - - #[test] - fn test_iflytek_astron_json_deserializes() { - let json = include_str!("../providers/declarative/iflytek_astron.json"); - let config: DeclarativeProviderConfig = - serde_json::from_str(json).expect("iflytek_astron.json should parse"); - assert_eq!(config.name, "iflytek_astron"); - assert_eq!(config.display_name, "iFlytek Astron MaaS"); - assert!(matches!(config.engine, ProviderEngine::OpenAI)); - assert_eq!(config.api_key_env, "ASTRON_API_KEY"); - assert_eq!(config.base_url, "${ASTRON_BASE_URL}"); - assert_eq!(config.dynamic_models, Some(false)); - assert!(config.skip_canonical_filtering); - assert_eq!(config.supports_streaming, Some(true)); - - let env_vars = config.env_vars.as_ref().expect("env_vars should be set"); - assert_eq!(env_vars.len(), 1); - assert_eq!(env_vars[0].name, "ASTRON_BASE_URL"); - assert!(!env_vars[0].required); - assert!(!env_vars[0].secret); - assert_eq!( - env_vars[0].default, - Some("https://maas-token-api.cn-huabei-1.xf-yun.com/v2".to_string()) + fn test_all_bundled_providers_deserialize() { + // `load_fixed_providers` silently skips any bundled JSON that fails to + // deserialize (it only emits a `warn!`), so a malformed provider file would + // ship as a missing provider rather than a build/test failure. Assert every + // bundled file parses through the same path the loader uses. + let mut failures = Vec::new(); + for file in FIXED_PROVIDERS.files() { + if file.path().extension().and_then(|s| s.to_str()) != Some("json") { + continue; + } + let content = file + .contents_utf8() + .unwrap_or_else(|| panic!("bundled provider {:?} is not valid UTF-8", file.path())); + if let Err(e) = deserialize_provider_config(content) { + failures.push(format!("{:?}: {e}", file.path())); + } + } + assert!( + failures.is_empty(), + "bundled declarative providers failed to deserialize:\n{}", + failures.join("\n") ); - assert!(config.models.iter().any(|m| m.name == "xsparkx2")); - assert!(config.models.iter().any(|m| m.name == "astron-code-latest")); } #[test] diff --git a/crates/goose/src/providers/declarative/iflytek.json b/crates/goose/src/providers/declarative/iflytek.json index 98a4cc62618e..23d355d60854 100644 --- a/crates/goose/src/providers/declarative/iflytek.json +++ b/crates/goose/src/providers/declarative/iflytek.json @@ -2,7 +2,7 @@ "name": "iflytek", "engine": "openai", "display_name": "iFlytek Spark", - "description": "iFlytek Spark (讯飞星火) models via the OpenAI-compatible HTTP API. Authenticate with your Spark HTTP API password (APIPassword).", + "description": "iFlytek Spark (讯飞星火) models via the OpenAI-compatible HTTP API. Authenticate with your Spark HTTP API password (APIPassword). Only the 4.0Ultra and Max model families are listed, since per the Spark HTTP docs they are the versions that accept system messages and Function Call (tools) — which goose always sends.", "api_key_env": "SPARK_API_PASSWORD", "base_url": "https://spark-api-open.xf-yun.com/v1", "dynamic_models": false, @@ -10,10 +10,7 @@ "models": [ {"name": "4.0Ultra", "context_limit": 8192}, {"name": "generalv3.5", "context_limit": 8192}, - {"name": "max-32k", "context_limit": 32768}, - {"name": "generalv3", "context_limit": 8192}, - {"name": "pro-128k", "context_limit": 131072}, - {"name": "lite", "context_limit": 4096} + {"name": "max-32k", "context_limit": 32768} ], "supports_streaming": true, "model_doc_link": "https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html", From 8b0b02e5a7128572928ce05e19dd7b0394849cad Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Thu, 25 Jun 2026 23:25:16 +0800 Subject: [PATCH 3/4] docs: note Spark tool_calls_switch limitation and sync provider list Spark only returns OpenAI-style tool_calls when the request body sets tool_calls_switch=true, which the declarative config cannot inject, so the iflytek provider is best used for chat. Note this in the provider description and the providers table, and drop the stale 'lite' mention (lite was removed from iflytek.json earlier). Signed-off-by: FenjuFu --- crates/goose/src/providers/declarative/iflytek.json | 2 +- documentation/docs/getting-started/providers.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/goose/src/providers/declarative/iflytek.json b/crates/goose/src/providers/declarative/iflytek.json index 23d355d60854..a0c2da038744 100644 --- a/crates/goose/src/providers/declarative/iflytek.json +++ b/crates/goose/src/providers/declarative/iflytek.json @@ -2,7 +2,7 @@ "name": "iflytek", "engine": "openai", "display_name": "iFlytek Spark", - "description": "iFlytek Spark (讯飞星火) models via the OpenAI-compatible HTTP API. Authenticate with your Spark HTTP API password (APIPassword). Only the 4.0Ultra and Max model families are listed, since per the Spark HTTP docs they are the versions that accept system messages and Function Call (tools) — which goose always sends.", + "description": "iFlytek Spark (讯飞星火) models via the OpenAI-compatible HTTP API. Authenticate with your Spark HTTP API password (APIPassword). Lists the 4.0Ultra and Max families, which per the Spark HTTP docs accept system messages (always sent by goose). Best used for chat: Spark only returns OpenAI-style tool_calls when the request body sets tool_calls_switch=true, which this declarative config cannot inject, so tool-using extensions may not work as-is.", "api_key_env": "SPARK_API_PASSWORD", "base_url": "https://spark-api-open.xf-yun.com/v1", "dynamic_models": false, diff --git a/documentation/docs/getting-started/providers.md b/documentation/docs/getting-started/providers.md index ec625b53c6a2..895fc18eabae 100644 --- a/documentation/docs/getting-started/providers.md +++ b/documentation/docs/getting-started/providers.md @@ -36,7 +36,7 @@ goose is compatible with a wide range of LLM providers, allowing you to choose a | [GCP Vertex AI](https://cloud.google.com/vertex-ai) | Google Cloud's Vertex AI platform, supporting Gemini and Claude models. **Credentials must be [configured in advance](https://cloud.google.com/vertex-ai/docs/authentication).** Filters for allowed models by organization policy (if configured). | `GCP_PROJECT_ID`, `GCP_LOCATION` and optionally `GCP_MAX_RATE_LIMIT_RETRIES` (5), `GCP_MAX_OVERLOADED_RETRIES` (5), `GCP_INITIAL_RETRY_INTERVAL_MS` (5000), `GCP_BACKOFF_MULTIPLIER` (2.0), `GCP_MAX_RETRY_INTERVAL_MS` (320_000). | | [GitHub Copilot](https://docs.github.com/en/copilot/using-github-copilot/ai-models) | Access to AI models from OpenAI, Anthropic, Google, and other providers through GitHub's Copilot infrastructure. **GitHub account with Copilot access required.** | No manual key. Uses [device flow authentication](#github-copilot-authentication) for both CLI and Desktop. | | [Groq](https://groq.com/) | High-performance inference hardware and tools for LLMs. | `GROQ_API_KEY` | -| [iFlytek Spark](https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html) | iFlytek Spark (讯飞星火) models via the OpenAI-compatible HTTP API, including 4.0Ultra, generalv3.5, and lite. | `SPARK_API_PASSWORD` | +| [iFlytek Spark](https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html) | iFlytek Spark (讯飞星火) models (4.0Ultra, generalv3.5, max-32k) via the OpenAI-compatible HTTP API. Best for chat: Spark needs `tool_calls_switch=true` (not injectable here) to return OpenAI-style tool calls. | `SPARK_API_PASSWORD` | | [iFlytek Astron MaaS](https://maas.xfyun.cn/) | iFlytek Astron MaaS (讯飞星辰) hosting Spark X2, DeepSeek, GLM, Kimi, MiniMax, Qwen, and Astron coding models via an OpenAI-compatible API. Set `ASTRON_BASE_URL` to switch between the Token Plan and Coding Plan endpoints. | `ASTRON_API_KEY`, `ASTRON_BASE_URL` (optional) | | [LiteLLM](https://docs.litellm.ai/docs/) | LiteLLM proxy supporting multiple models with automatic prompt caching and unified API access. | `LITELLM_HOST`, `LITELLM_BASE_PATH` (optional), `LITELLM_API_KEY` (optional), `LITELLM_CUSTOM_HEADERS` (optional), `LITELLM_TIMEOUT` (optional) | | [LM Studio](https://lmstudio.ai/) | Run local models with LM Studio's OpenAI-compatible server. **Because this provider runs locally, you must first [download a model](#local-llms).** | None required. Connects to local server at `localhost:1234` by default. | From c80f20f81d318227dfa3fb490c7f71fc7b8f27af Mon Sep 17 00:00:00 2001 From: Douwe M Osinga Date: Tue, 30 Jun 2026 15:37:46 -0400 Subject: [PATCH 4/4] fix: cap MiniMax M2.5 context limit at documented 128K Token Plan size The xminimaxm25 model was listed with a 204800 context limit, but the Astron Token Plan documents a 128K (131072) cap for this model. Lower it to match. --- crates/goose/src/providers/declarative/iflytek_astron.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/goose/src/providers/declarative/iflytek_astron.json b/crates/goose/src/providers/declarative/iflytek_astron.json index a239644e77be..6cffd3ba6af0 100644 --- a/crates/goose/src/providers/declarative/iflytek_astron.json +++ b/crates/goose/src/providers/declarative/iflytek_astron.json @@ -25,7 +25,7 @@ {"name": "xopdeepseekv4flash", "context_limit": 131072}, {"name": "xopdeepseekv32", "context_limit": 131072}, {"name": "xopkimik26", "context_limit": 262144}, - {"name": "xminimaxm25", "context_limit": 204800}, + {"name": "xminimaxm25", "context_limit": 131072}, {"name": "xopqwen35397b", "context_limit": 262144}, {"name": "astron-code-latest", "context_limit": 131072} ],