mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-29 03:12:10 +08:00
Add Qiniu Cloud AI and Fenno provider presets
This commit is contained in:
@@ -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&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>
|
||||
|
||||
@@ -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&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&base_url=https%3A%2F%2Fapi.qnaigc.com&protocol=openai_chat_completions&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&base_url=https%3A%2F%2Fapi.fenno.ai&protocol=openai_chat_completions&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&base_url=https%3A%2F%2Fapi.qnaigc.com&protocol=openai_chat_completions&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&base_url=https%3A%2F%2Fapi.fenno.ai&protocol=openai_chat_completions&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>
|
||||
|
||||
## 嵌入式按钮组件
|
||||
|
||||
Generated
+4
-4
@@ -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
@@ -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",
|
||||
|
||||
@@ -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&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>
|
||||
|
||||
|
||||
@@ -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&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"
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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([]),
|
||||
|
||||
Reference in New Issue
Block a user