Add Qiniu Cloud AI and Fenno provider presets

This commit is contained in:
musistudio
2026-07-10 23:14:35 +08:00
parent 222ccfa208
commit 2bb2611447
24 changed files with 294 additions and 25 deletions
+16
View File
@@ -224,6 +224,22 @@ Codex support is powered by [musistudio/codexl](https://github.com/musistudio/co
</a>
</td>
</tr>
<tr>
<td align="center" width="330">
<a href="https://s.qiniu.com/AVjMVf">
<img src="/docs/public/provider-icons/qiniu-ai.png" width="42" height="42" alt="Qiniu Cloud AI icon" />
<br />
<strong>Qiniu Cloud AI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&amp;aff=9HHHAB5QLAES">
<img src="/docs/public/provider-icons/fenno.jpg" width="42" height="42" alt="Fenno.ai icon" />
<br />
<strong>Fenno.ai</strong>
</a>
</td>
</tr>
</table>
<h4>Community Sponsors</h4>
+16
View File
@@ -223,6 +223,22 @@ CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序
</a>
</td>
</tr>
<tr>
<td align="center" width="330">
<a href="https://s.qiniu.com/AVjMVf">
<img src="/docs/public/provider-icons/qiniu-ai.png" width="42" height="42" alt="七牛云 AI 图标" />
<br />
<strong>七牛云 AI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&amp;aff=9HHHAB5QLAES">
<img src="/docs/public/provider-icons/fenno.jpg" width="42" height="42" alt="Fenno.ai 图标" />
<br />
<strong>Fenno.ai</strong>
</a>
</td>
</tr>
</table>
<h4>社区赞助者</h4>
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -86,6 +86,14 @@ Choose a provider below to get started. CCR shows what will be added before savi
<span class="provider-import-icon-shell"><img src="../../../provider-icons/claudeapi.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">claudeapi</span><span class="provider-import-meta">Anthropic Messages</span></span>
</a>
<a class="provider-import-button provider-qiniu-ai" href="ccr://provider?name=%E4%B8%83%E7%89%9B%E4%BA%91+AI&amp;base_url=https%3A%2F%2Fapi.qnaigc.com&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Fs.qiniu.com%2FAVjMVf" aria-label="Import Qiniu Cloud AI provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/qiniu-ai.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Qiniu Cloud AI</span><span class="provider-import-meta">Chat / Responses / Anthropic / Gemini Generate</span></span>
</a>
<a class="provider-import-button provider-fenno" href="ccr://provider?name=Fenno.ai&amp;base_url=https%3A%2F%2Fapi.fenno.ai&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Fapi.fenno.ai%2Fregister%3Fredirect%3D%2Fpurchase%3Ftab%3Dsubscription%2526group%3D16%26aff%3D9HHHAB5QLAES" aria-label="Import Fenno.ai provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/fenno.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Fenno.ai</span><span class="provider-import-meta">Chat / Responses / Anthropic</span></span>
</a>
</div>
## Embeddable Button Component
@@ -86,6 +86,14 @@ lead: 快速添加常见模型供应商,确认无误后即可保存,减少
<span class="provider-import-icon-shell"><img src="../../provider-icons/claudeapi.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">claudeapi</span><span class="provider-import-meta">Anthropic Messages</span></span>
</a>
<a class="provider-import-button provider-qiniu-ai" href="ccr://provider?name=%E4%B8%83%E7%89%9B%E4%BA%91+AI&amp;base_url=https%3A%2F%2Fapi.qnaigc.com&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Fs.qiniu.com%2FAVjMVf" aria-label="导入七牛云 AI 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/qiniu-ai.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">七牛云 AI</span><span class="provider-import-meta">Chat / Responses / Anthropic / Gemini Generate</span></span>
</a>
<a class="provider-import-button provider-fenno" href="ccr://provider?name=Fenno.ai&amp;base_url=https%3A%2F%2Fapi.fenno.ai&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Fapi.fenno.ai%2Fregister%3Fredirect%3D%2Fpurchase%3Ftab%3Dsubscription%2526group%3D16%26aff%3D9HHHAB5QLAES" aria-label="导入 Fenno.ai 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/fenno.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Fenno.ai</span><span class="provider-import-meta">Chat / Responses / Anthropic</span></span>
</a>
</div>
## 嵌入式按钮组件
+4 -4
View File
@@ -12,7 +12,7 @@
"packages/*"
],
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.6",
"@the-next-ai/ai-gateway": "^1.0.7",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"electron-updater": "^6.8.9",
@@ -2310,9 +2310,9 @@
}
},
"node_modules/@the-next-ai/ai-gateway": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@the-next-ai/ai-gateway/-/ai-gateway-1.0.6.tgz",
"integrity": "sha512-EIJjwvc//hql1jwE94yyMWNGrJLuCJQlTNgn8p3KWhDYSiOWp4vmLU2Gw3/A+J49X21V7HYdX/W9YuJUai80eQ==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@the-next-ai/ai-gateway/-/ai-gateway-1.0.7.tgz",
"integrity": "sha512-HJtfjpBgjFEPQ6ub7nFTtEyxzzbrz6V5vQZfJOoHi3Jhpjob3W4h0UenTW6loHanJYucFIEh01SvB//Qj3ulcw==",
"license": "MIT",
"dependencies": {
"diff": "^8.0.3",
+1 -1
View File
@@ -56,7 +56,7 @@
"rebuild:sqlite3": "electron-rebuild -f -w better-sqlite3"
},
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.6",
"@the-next-ai/ai-gateway": "^1.0.7",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"electron-updater": "^6.8.9",
+14
View File
@@ -208,6 +208,20 @@ Codex support is powered by [musistudio/codexl](https://github.com/musistudio/co
<strong>TeamoRouter</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://s.qiniu.com/AVjMVf">
<img src="/docs/public/provider-icons/qiniu-ai.png" width="42" height="42" alt="Qiniu Cloud AI icon" />
<br />
<strong>Qiniu Cloud AI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&amp;aff=9HHHAB5QLAES">
<img src="/docs/public/provider-icons/fenno.jpg" width="42" height="42" alt="Fenno.ai icon" />
<br />
<strong>Fenno.ai</strong>
</a>
</td>
</tr>
</table>
+14
View File
@@ -207,6 +207,20 @@ CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序
<strong>TeamoRouter</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://s.qiniu.com/AVjMVf">
<img src="/docs/public/provider-icons/qiniu-ai.png" width="42" height="42" alt="七牛云 AI 图标" />
<br />
<strong>七牛云 AI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&amp;aff=9HHHAB5QLAES">
<img src="/docs/public/provider-icons/fenno.jpg" width="42" height="42" alt="Fenno.ai 图标" />
<br />
<strong>Fenno.ai</strong>
</a>
</td>
</tr>
</table>
@@ -0,0 +1,15 @@
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
export const fennoProviderPreset: ProviderPreset = {
account: defaultProviderAccountConfig,
aliases: ["fenno", "fenno.ai", "fenno ai"],
endpoints: [
{
baseUrl: "https://api.fenno.ai",
protocols: ["openai_chat_completions", "openai_responses", "anthropic_messages"]
}
],
id: "fenno",
name: "Fenno.ai",
websiteUrl: "https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=9HHHAB5QLAES"
};
@@ -3,6 +3,7 @@ import { bailianProviderPreset } from "@ccr/core/providers/presets/bailian/index
import { claudeApiProviderPreset } from "@ccr/core/providers/presets/claudeapi/index";
import { code0ProviderPreset } from "@ccr/core/providers/presets/code0/index";
import { deepSeekProviderPreset } from "@ccr/core/providers/presets/deepseek/index";
import { fennoProviderPreset } from "@ccr/core/providers/presets/fenno/index";
import { geminiProviderPreset } from "@ccr/core/providers/presets/gemini/index";
import { kimiCodingProviderPreset } from "@ccr/core/providers/presets/kimi-coding/index";
import { minimaxChinaProviderPreset, minimaxGlobalProviderPreset } from "@ccr/core/providers/presets/minimax/index";
@@ -10,6 +11,7 @@ import { mistralProviderPreset } from "@ccr/core/providers/presets/mistral/index
import { moonshotChinaProviderPreset, moonshotGlobalProviderPreset } from "@ccr/core/providers/presets/moonshot/index";
import { openaiProviderPreset } from "@ccr/core/providers/presets/openai/index";
import { openRouterProviderPreset } from "@ccr/core/providers/presets/openrouter/index";
import { qiniuAiProviderPreset } from "@ccr/core/providers/presets/qiniu-ai/index";
import { runApiProviderPreset } from "@ccr/core/providers/presets/runapi/index";
import { siliconFlowProviderPreset } from "@ccr/core/providers/presets/siliconflow/index";
import { teamoRouterProviderPreset } from "@ccr/core/providers/presets/teamorouter/index";
@@ -46,6 +48,8 @@ export const providerPresets: ProviderPreset[] = [
moonshotGlobalProviderPreset,
bailianProviderPreset,
siliconFlowProviderPreset,
qiniuAiProviderPreset,
fennoProviderPreset,
runApiProviderPreset,
teamoRouterProviderPreset,
code0ProviderPreset,
@@ -0,0 +1,35 @@
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
export const qiniuAiProviderPreset: ProviderPreset = {
account: defaultProviderAccountConfig,
aliases: ["qiniu", "qiniu ai", "qiniu cloud ai", "qiniu yun ai", "qiniu yun", "七牛云", "七牛云ai", "七牛云 ai", "modelink"],
endpoints: [
{
baseUrl: "https://api.qnaigc.com",
label: "China mainland OpenAI",
protocols: ["openai_chat_completions"],
websiteUrl: "https://s.qiniu.com/AVjMVf"
},
{
baseUrl: "https://api.qnaigc.com/bypass/openai/v1",
label: "China mainland OpenAI Responses",
protocols: ["openai_responses"],
websiteUrl: "https://s.qiniu.com/AVjMVf"
},
{
baseUrl: "https://api.qnaigc.com",
label: "China mainland Anthropic",
protocols: ["anthropic_messages"],
websiteUrl: "https://s.qiniu.com/AVjMVf"
},
{
baseUrl: "https://api.qnaigc.com/bypass/vertex/v1",
label: "China mainland Gemini Generate",
protocols: ["gemini_generate_content"],
websiteUrl: "https://s.qiniu.com/AVjMVf"
}
],
id: "qiniu-ai",
name: "七牛云 AI",
websiteUrl: "https://s.qiniu.com/AVjMVf"
};
+12 -4
View File
@@ -465,7 +465,7 @@ async function fetchModelsForSource(parsed: ParsedProviderUrl, source: ModelSour
};
}
const result = await requestJson(withGeminiKey(`${parsed.geminiBaseUrl}/v1beta/models`, apiKey), {
const result = await requestJson(withGeminiKey(geminiApiEndpoint(parsed.geminiBaseUrl, "models"), apiKey), {
headers: {
...geminiHeaders(apiKey)
},
@@ -762,11 +762,11 @@ function endpointsForProtocol(
return [
{
baseUrl: parsed.geminiBaseUrl,
endpoint: `${parsed.geminiBaseUrl}/v1beta/interactions`
endpoint: geminiApiEndpoint(parsed.geminiBaseUrl, "interactions", "v1beta")
},
{
baseUrl: parsed.geminiBaseUrl,
endpoint: `${parsed.geminiBaseUrl}/v1/interactions`
endpoint: geminiApiEndpoint(parsed.geminiBaseUrl, "interactions", "v1")
}
];
}
@@ -775,11 +775,19 @@ function endpointsForProtocol(
return [
{
baseUrl: parsed.geminiBaseUrl,
endpoint: `${parsed.geminiBaseUrl}/v1beta/models/${encodedModel}:generateContent`
endpoint: geminiApiEndpoint(parsed.geminiBaseUrl, `models/${encodedModel}:generateContent`)
}
];
}
function geminiApiEndpoint(baseUrl: string, path: string, defaultVersion: "v1" | "v1beta" = "v1beta"): string {
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
if (/\/v1(?:beta)?$/i.test(normalizedBaseUrl)) {
return `${normalizedBaseUrl}/${path}`;
}
return `${normalizedBaseUrl}/${defaultVersion}/${path}`;
}
function withGeminiKey(url: string, apiKey: string | undefined): string {
if (!apiKey) {
return url;
+22 -1
View File
@@ -39,7 +39,7 @@ export function parseProviderBaseUrl(value: string): ParsedProviderBaseUrl {
return {
anthropicBaseUrl,
anthropicBaseUrlCandidates,
geminiBaseUrl: rootBaseUrl,
geminiBaseUrl: providerGeminiBaseUrl(normalizedInputBaseUrl, rootBaseUrl),
normalizedInputBaseUrl,
openaiBaseUrl,
openaiBaseUrlCandidates,
@@ -126,6 +126,27 @@ function stripProviderApiVersion(value: string): string {
return compactProviderUrl(url);
}
function providerGeminiBaseUrl(normalizedInputBaseUrl: string, rootBaseUrl: string): string {
return isVersionedVertexBypassBaseUrl(normalizedInputBaseUrl)
? normalizedInputBaseUrl
: rootBaseUrl;
}
function isVersionedVertexBypassBaseUrl(value: string): boolean {
try {
const url = new URL(value);
const segments = url.pathname
.split("/")
.map((segment) => segment.trim().toLowerCase())
.filter(Boolean);
return segments.includes("bypass") &&
segments.includes("vertex") &&
/^(v1|v1beta)$/.test(segments[segments.length - 1] ?? "");
} catch {
return false;
}
}
function stripNestedProviderApiVersion(pathname: string): string {
return pathname.replace(/(\/v[0-9][a-z0-9-]*)\/v1$/i, "$1") || "/";
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -610,12 +610,6 @@ export function ProviderDeepLinkDialog({
</div>
</div>
) : null}
{showExternalProviderWarnings ? (
<div className="flex items-start gap-2 rounded-md border border-border bg-background px-3 py-2 text-[11px] leading-4 text-muted-foreground">
<CircleAlert className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>{t("Only enter an API key issued for this endpoint. Official provider keys must only be used with official endpoints.")}</span>
</div>
) : null}
<div className="flex min-w-0 items-center gap-3 rounded-md border border-border bg-background px-3 py-2.5">
<div className="relative shrink-0">
<ProviderPresetIcon className="h-10 w-10 rounded-md" iconUrl={providerIconUrl} preset={providerPreset} />
@@ -1575,10 +1569,6 @@ export function AddProviderForm({
) : null}
<Field className="sm:col-span-2" label={t("API key")}>
<Input type="password" value={draft.apiKey} onChange={(event) => onChange({ apiKey: event.target.value }, true)} />
<div className="flex items-start gap-1.5 text-[11px] leading-4 text-muted-foreground">
<CircleAlert className="mt-0.5 h-3 w-3 shrink-0" />
<span>{t("Only enter an API key issued for this endpoint. Official provider keys must only be used with official endpoints.")}</span>
</div>
</Field>
{safetyIssue ? (
<div className="sm:col-span-2 flex items-start gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-[12px] leading-5 text-amber-900 dark:text-amber-100">
@@ -1719,8 +1709,8 @@ export function AddProviderForm({
{protocolProbeRows.length ? (
<div className="space-y-1.5">
{protocolProbeRows.map((item) => {
const available = item.supported || selectableProtocols.includes(item.protocol);
const selectable = available && selectableProtocols.includes(item.protocol);
const available = item.supported;
const selectable = item.supported && selectableProtocols.includes(item.protocol);
const checked = selectable && draft.selectedProtocols.includes(item.protocol);
const itemKey = `${item.protocol}-${item.endpoint}`;
return (
@@ -587,7 +587,6 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"API key created": "API 密钥已创建",
"API keys database": "API 密钥数据库",
"Copy this key now. It may not be shown again.": "请现在复制保存这个密钥,之后可能不会再次完整显示。",
"Only enter an API key issued for this endpoint. Official provider keys must only be used with official endpoints.": "只输入由当前端点签发的 API 密钥。官方供应商密钥只能用于官方端点。",
"Add": "添加",
"Add env variable": "添加环境变量",
"Add header": "添加请求头",
@@ -46,11 +46,13 @@ import bailianProviderIconUrl from "@/assets/provider-icons/bailian.ico";
import claudeapiProviderIconUrl from "@/assets/provider-icons/claudeapi.png";
import code0ProviderIconUrl from "@/assets/provider-icons/code0.png";
import deepseekProviderIconUrl from "@/assets/provider-icons/deepseek.ico";
import fennoProviderIconUrl from "@/assets/provider-icons/fenno.jpg";
import geminiProviderIconUrl from "@/assets/provider-icons/gemini.svg";
import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
import qiniuAiProviderIconUrl from "@/assets/provider-icons/qiniu-ai.png";
import runapiProviderIconUrl from "@/assets/provider-icons/runapi.jpg";
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
import teamorouterProviderIconUrl from "@/assets/provider-icons/teamorouter.png";
@@ -324,6 +326,7 @@ export const providerPresetIconUrls: Record<string, string> = {
claudeapi: claudeapiProviderIconUrl,
code0: code0ProviderIconUrl,
deepseek: deepseekProviderIconUrl,
fenno: fennoProviderIconUrl,
gemini: geminiProviderIconUrl,
"kimi-coding": moonshotProviderIconUrl,
mistral: mistralProviderIconUrl,
@@ -331,6 +334,7 @@ export const providerPresetIconUrls: Record<string, string> = {
"moonshot-global": moonshotProviderIconUrl,
openai: openaiProviderIconUrl,
openrouter: openrouterProviderIconUrl,
"qiniu-ai": qiniuAiProviderIconUrl,
runapi: runapiProviderIconUrl,
siliconflow: siliconflowProviderIconUrl,
teamorouter: teamorouterProviderIconUrl,
@@ -1716,10 +1716,11 @@ export function providerProbeCandidates(draft: AddProviderDraft): ProviderProbeC
const preset = findProviderPreset(draft.presetId);
const protocols = providerProtocolOptions.map((option) => option.value);
if (preset) {
const probeAllProtocols = preset.endpoints.length === 1;
return preset.endpoints.map((endpoint) => ({
...endpoint,
declaredProtocols: endpoint.protocols,
protocols,
protocols: probeAllProtocols ? protocols : endpoint.protocols,
source: "preset"
}));
}
+30
View File
@@ -8,10 +8,16 @@ import {
providerIdentitySafetyIssueInList,
providerPresetMatchesBaseUrl
} from "../../packages/core/src/providers/presets/utils.ts";
import {
fennoProviderPreset
} from "../../packages/core/src/providers/presets/fenno/index.ts";
import {
moonshotChinaProviderPreset,
moonshotGlobalProviderPreset
} from "../../packages/core/src/providers/presets/moonshot/index.ts";
import {
qiniuAiProviderPreset
} from "../../packages/core/src/providers/presets/qiniu-ai/index.ts";
const openAiPreset = {
aliases: ["OpenAI", "ChatGPT"],
@@ -57,6 +63,30 @@ test("provider identity lookup prefers exact Kimi regional names over shared ali
assert.equal(findProviderPresetByIdentityInList(moonshotPresets, "Kimi API (China)")?.id, "moonshot");
});
test("sponsor provider presets expose requested endpoints and protocols", () => {
assert.equal(fennoProviderPreset.websiteUrl, "https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=9HHHAB5QLAES");
assert.deepEqual(fennoProviderPreset.endpoints[0]?.protocols, [
"openai_chat_completions",
"openai_responses",
"anthropic_messages"
]);
assert.equal(qiniuAiProviderPreset.websiteUrl, "https://s.qiniu.com/AVjMVf");
assert.equal(providerPresetMatchesBaseUrl(qiniuAiProviderPreset, "https://api.qnaigc.com"), true);
assert.equal(providerPresetMatchesBaseUrl(qiniuAiProviderPreset, "https://api.modelink.ai/v1/models"), false);
assert.equal(providerPresetMatchesBaseUrl(qiniuAiProviderPreset, "https://api.qnaigc.com/bypass/openai/v1/responses"), true);
assert.equal(providerPresetMatchesBaseUrl(qiniuAiProviderPreset, "https://api.qnaigc.com/bypass/vertex/v1/models/gemini-pro:generateContent"), true);
assert.deepEqual(qiniuAiProviderPreset.endpoints.map((endpoint) => [endpoint.label, endpoint.baseUrl, endpoint.protocols]), [
["China mainland OpenAI", "https://api.qnaigc.com", ["openai_chat_completions"]],
["China mainland OpenAI Responses", "https://api.qnaigc.com/bypass/openai/v1", ["openai_responses"]],
["China mainland Anthropic", "https://api.qnaigc.com", ["anthropic_messages"]],
["China mainland Gemini Generate", "https://api.qnaigc.com/bypass/vertex/v1", ["gemini_generate_content"]]
]);
assert.deepEqual(qiniuAiProviderPreset.endpoints[0]?.protocols, [
"openai_chat_completions"
]);
});
test("provider identity safety does not block branded third-party endpoints", () => {
assert.equal(
providerIdentitySafetyIssueInList(presets, {
+12
View File
@@ -27,6 +27,18 @@ test("provider URL parsing handles local and Gemini endpoint variants", () => {
assert.equal(parsed.geminiBaseUrl, "http://localhost:8787");
});
test("provider URL parsing preserves versioned Vertex bypass bases for Gemini", () => {
const parsed = parseProviderBaseUrl("https://api.qnaigc.com/bypass/vertex/v1/models/gemini-pro:generateContent");
assert.equal(parsed.normalizedInputBaseUrl, "https://api.qnaigc.com/bypass/vertex/v1");
assert.equal(parsed.rootBaseUrl, "https://api.qnaigc.com/bypass/vertex");
assert.equal(parsed.geminiBaseUrl, "https://api.qnaigc.com/bypass/vertex/v1");
assert.equal(
normalizeProviderBaseUrl("https://api.qnaigc.com/bypass/vertex/v1", "gemini_generate_content"),
"https://api.qnaigc.com/bypass/vertex/v1"
);
});
test("provider URL parsing handles Gemini Interactions endpoint variants", () => {
const parsed = parseProviderBaseUrl("localhost:8787/v1/interactions/interaction-123/cancel");
+75 -1
View File
@@ -5,7 +5,8 @@ import { renderToStaticMarkup } from "react-dom/server";
import { newApiKeyUsageAccountConfig } from "../../packages/core/src/providers/new-api.ts";
import { geminiProviderPreset } from "../../packages/core/src/providers/presets/gemini/index.ts";
import { moonshotGlobalProviderPreset } from "../../packages/core/src/providers/presets/moonshot/index.ts";
import { ProvidersView } from "../../packages/ui/src/pages/home/components/providers.tsx";
import { qiniuAiProviderPreset } from "../../packages/core/src/providers/presets/qiniu-ai/index.ts";
import { AddProviderDialog, ProvidersView } from "../../packages/ui/src/pages/home/components/providers.tsx";
import {
applyProviderProbeResult,
createProviderConfigFromDeepLink,
@@ -40,6 +41,22 @@ test("Gemini preset keeps full protocol probing candidates", () => {
assert.deepEqual(candidates[0].declaredProtocols, ["gemini_generate_content", "gemini_interactions"]);
});
test("multi-endpoint presets probe only each endpoint's declared protocols", () => {
setProviderPresets([qiniuAiProviderPreset]);
const draft = {
...createProviderDraft([]),
presetId: "qiniu-ai"
};
const candidates = providerProbeCandidates(draft);
assert.equal(candidates.length, qiniuAiProviderPreset.endpoints.length);
assert.deepEqual(
candidates.map((candidate) => [candidate.baseUrl, candidate.protocols]),
qiniuAiProviderPreset.endpoints.map((endpoint) => [endpoint.baseUrl, endpoint.protocols])
);
});
test("provider probe result drops unavailable selected protocols", () => {
const draft = {
...createProviderDraft([]),
@@ -90,6 +107,63 @@ test("provider probe result keeps only supported selected protocols", () => {
assert.deepEqual(next.selectedProtocols, ["gemini_generate_content"]);
});
test("provider protocol details keep failed endpoint rows unavailable", () => {
const draft = {
...createProviderDraft([]),
baseUrl: "https://api.example.com/v1",
name: "Example",
protocol: "openai_chat_completions" as const,
selectedProtocols: ["openai_chat_completions" as const]
};
const html = renderToStaticMarkup(
React.createElement(AddProviderDialog, {
canSubmit: true,
draft,
error: "",
mode: "edit",
onChange: () => undefined,
onClose: () => undefined,
onSubmit: async () => true,
probe: {
capabilities: [
{
baseUrl: "https://api.example.com/v1",
endpoint: "https://api.example.com/v1/chat/completions",
source: "detected" as const,
type: "openai_chat_completions" as const
}
],
detectedProtocol: "openai_chat_completions" as const,
models: [],
normalizedBaseUrl: "https://api.example.com/v1",
protocols: [
{
baseUrl: "https://api.example.com",
endpoint: "https://api.example.com/chat/completions",
message: "HTTP 404",
protocol: "openai_chat_completions" as const,
status: 404,
supported: false
},
{
baseUrl: "https://api.example.com/v1",
endpoint: "https://api.example.com/v1/chat/completions",
message: "HTTP 400: model is required",
protocol: "openai_chat_completions" as const,
status: 400,
supported: true
}
]
},
probeLoading: false,
providers: []
})
);
assert.match(html, /Unavailable/);
assert.match(html, /Available/);
});
test("provider probe result applies detected New API key quota account connector", () => {
const draft = {
...createProviderDraft([]),