fix(providers): route iflytek/sparkdesk to Spark's OpenAI-compatible host (#7942)

Both entries declare format: "openai" with authHeader: "bearer", but
pointed at spark-api.xf-yun.com — Spark's WebSocket host, which
authenticates with an HMAC-SHA256 signature over app_id/apiKey/apiSecret
and rejects bearer tokens. The OpenAI-compatible HTTP API lives on
spark-api-open.xf-yun.com/v1, so neither provider could complete a
request as configured.

sparkdesk also listed a "general" model; that is a WebSocket domain
value and is not accepted by the HTTP endpoint, so it becomes "lite"
(Spark Lite). The free-model catalog's sparkdesk row is updated to
match, and a regression test locks both baseUrls, the removed "general"
model id, and catalog/registry cross-reference.

Reconstructed against release/v3.8.49 (folds in the same-PR follow-up
"point sparkdesk free-catalog row at lite").

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
FenjuFu
2026-07-22 00:16:52 +08:00
committed by GitHub
parent a865fddb26
commit 19cbe8ae14
6 changed files with 73 additions and 13 deletions

View File

@@ -0,0 +1 @@
- **fix(providers):** iFlytek Spark (`iflytek`, `sparkdesk`) now target the OpenAI-compatible host `spark-api-open.xf-yun.com/v1`; the previous `spark-api.xf-yun.com` is the WebSocket host and rejects bearer auth ([#7942](https://github.com/diegosouzapw/OmniRoute/pull/7942)) — thanks @FenjuFu

View File

@@ -443,7 +443,7 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "siliconflow", modelId: "zai-org/GLM-4.7", displayName: "GLM 4.7", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "siliconflow", tos: "caution" },
{ provider: "siliconflow", modelId: "openai/gpt-oss-120b", displayName: "GPT OSS 120B", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "siliconflow", tos: "caution" },
{ provider: "siliconflow", modelId: "baidu/ERNIE-4.5-300B-A47B", displayName: "ERNIE 4.5 300B", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "siliconflow", tos: "caution" },
{ provider: "sparkdesk", modelId: "general", displayName: "General", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "sparkdesk", tos: "caution" },
{ provider: "sparkdesk", modelId: "lite", displayName: "Spark Lite", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "sparkdesk", tos: "caution" },
{ provider: "stepfun", modelId: "step-1v", displayName: "Step 1V", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "stepfun", tos: "ok" },
{ provider: "t3-web", modelId: "claude-opus-4", displayName: "Claude Opus 4 (via t3.chat)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "t3-web", tos: "avoid" },
{ provider: "t3-web", modelId: "claude-sonnet-4", displayName: "Claude Sonnet 4 (via t3.chat)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "t3-web", tos: "avoid" },

View File

@@ -5,11 +5,15 @@ export const iflytekProvider: RegistryEntry = {
alias: "iflytek",
format: "openai",
executor: "default",
baseUrl: "https://spark-api.xf-yun.com/v1/chat/completions",
baseUrl: "https://spark-api-open.xf-yun.com/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
// Sweep 2026-06-19: confirmed exact HTTP `domain` values against the official
// xfyun.cn Spark docs. generalv3.5 = Max (current); 4.0Ultra is case-sensitive.
// `spark-api-open.xf-yun.com` is Spark's OpenAI-compatible HTTP endpoint (Bearer
// APIPassword). `spark-api.xf-yun.com` is the WebSocket host — `wss://.../v3.1/chat`
// etc., authenticated with an HMAC-SHA256 signature, not a bearer token — so it
// cannot serve this `format: "openai"` / `authHeader: "bearer"` entry.
// Model ids are the `domain` values accepted by the HTTP endpoint:
// generalv3.5 = Max (current); 4.0Ultra is case-sensitive.
models: [
{ id: "4.0Ultra", name: "Spark 4.0 Ultra", contextLength: 32768 },
{ id: "generalv3.5", name: "Spark Max (V3.5)" },

View File

@@ -5,16 +5,19 @@ export const sparkdeskProvider: RegistryEntry = {
alias: "sparkdesk",
format: "openai",
executor: "default",
baseUrl: "https://spark-api.xf-yun.com/v3.1/chat/completions",
baseUrl: "https://spark-api-open.xf-yun.com/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
// Sweep 2026-06-19: confirmed exact HTTP `domain` values against the official
// xfyun.cn Spark docs. `spark-x` was rejected — it lives on a separate /v2 (X1.5) /
// /x2 (X2) endpoint and would 404 on this /v3.1 base.
// `spark-api-open.xf-yun.com` is Spark's OpenAI-compatible HTTP endpoint (Bearer
// APIPassword). `spark-api.xf-yun.com` is the WebSocket host — `wss://.../v3.1/chat`
// etc., authenticated with an HMAC-SHA256 signature, not a bearer token — so it
// cannot serve this `format: "openai"` / `authHeader: "bearer"` entry.
// Model ids are the `domain` values accepted by the HTTP endpoint. `spark-x` is
// rejected: it lives on separate /v2 (X1.5) and /x2 (X2) endpoints.
models: [
{ id: "4.0Ultra", name: "Spark 4.0 Ultra", contextLength: 32768 },
{ id: "generalv3", name: "Spark Pro", contextLength: 8192 },
{ id: "pro-128k", name: "Spark Pro 128K", contextLength: 131072 },
{ id: "general", name: "General" },
{ id: "lite", name: "Spark Lite", contextLength: 4096 },
],
};

View File

@@ -2476,8 +2476,8 @@
}
},
"url": {
"nonStream": "https://spark-api.xf-yun.com/v1/chat/completions",
"stream": "https://spark-api.xf-yun.com/v1/chat/completions"
"nonStream": "https://spark-api-open.xf-yun.com/v1/chat/completions",
"stream": "https://spark-api-open.xf-yun.com/v1/chat/completions"
}
},
"inference-net": {
@@ -4152,8 +4152,8 @@
}
},
"url": {
"nonStream": "https://spark-api.xf-yun.com/v3.1/chat/completions",
"stream": "https://spark-api.xf-yun.com/v3.1/chat/completions"
"nonStream": "https://spark-api-open.xf-yun.com/v1/chat/completions",
"stream": "https://spark-api-open.xf-yun.com/v1/chat/completions"
}
},
"stepfun": {

View File

@@ -0,0 +1,52 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { iflytekProvider } from "../../open-sse/config/providers/registry/iflytek/index.ts";
import { sparkdeskProvider } from "../../open-sse/config/providers/registry/sparkdesk/index.ts";
import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.data.ts";
// #7942: both entries declare format: "openai" + authHeader: "bearer", but pointed at
// spark-api.xf-yun.com — Spark's WebSocket host, which authenticates with an
// HMAC-SHA256 signature over app_id/apiKey/apiSecret and rejects bearer tokens. The
// OpenAI-compatible HTTP API lives on spark-api-open.xf-yun.com/v1.
test("#7942: iflytek registry baseUrl uses Spark's OpenAI-compatible HTTP host", () => {
assert.equal(
iflytekProvider.baseUrl,
"https://spark-api-open.xf-yun.com/v1/chat/completions",
"iflytek must route through the OpenAI-compatible HTTP endpoint, not the " +
"WebSocket-only spark-api.xf-yun.com host (which rejects bearer auth)"
);
});
test("#7942: sparkdesk registry baseUrl uses Spark's OpenAI-compatible HTTP host", () => {
assert.equal(
sparkdeskProvider.baseUrl,
"https://spark-api-open.xf-yun.com/v1/chat/completions",
"sparkdesk must route through the OpenAI-compatible HTTP endpoint, not the " +
"WebSocket-only spark-api.xf-yun.com/v3.1 host (which rejects bearer auth)"
);
});
test("#7942: sparkdesk no longer advertises the WebSocket-only 'general' domain", () => {
const modelIds = sparkdeskProvider.models.map((m) => m.id);
assert.ok(
!modelIds.includes("general"),
"'general' is a WebSocket-domain value rejected by the HTTP endpoint"
);
assert.ok(
modelIds.includes("lite"),
"sparkdesk should advertise 'lite' (Spark Lite) in place of the removed 'general' domain"
);
});
test("#7942: free-model catalog's sparkdesk row references a model the registry still advertises", () => {
const sparkdeskModelIds = new Set(sparkdeskProvider.models.map((m) => m.id));
const catalogRows = FREE_MODEL_BUDGETS.filter((row) => row.provider === "sparkdesk");
assert.ok(catalogRows.length > 0, "expected at least one sparkdesk row in the free catalog");
for (const row of catalogRows) {
assert.ok(
sparkdeskModelIds.has(row.modelId),
`free catalog references sparkdesk/${row.modelId}, which the registry no longer advertises`
);
}
});