feat: add AIHubMix provider (#865)

* feat: add AIHubMix provider

* feat: load AIHubMix models dynamically

* feat: polish AIHubMix model setup

* feat: send AIHubMix app code

* docs: remove redundant AIHubMix recommendation

---------

Co-authored-by: LL <13697272357@163.com>
This commit is contained in:
YOYO-do
2026-06-15 12:54:18 +08:00
committed by GitHub
parent 988034cc3e
commit 0f9699843f
16 changed files with 606 additions and 21 deletions

View File

@@ -1,10 +1,36 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import {
getAIModel,
isAihubmixStandardBaseURL,
resolveBaseURL,
supportsImageInput,
supportsPromptCaching,
} from "@/lib/ai-providers"
import { extractAihubmixModelIds } from "@/lib/aihubmix-models"
describe("extractAihubmixModelIds", () => {
it("extracts unique chat model IDs from the AIHubMix model list payload", () => {
const models = extractAihubmixModelIds({
data: [
{ model_id: "claude-sonnet-4-5-20250929", types: "llm" },
{ model_id: "gpt-5.1", types: "llm" },
{ model_id: "gpt-5.1", types: "llm" },
{ model_id: "gpt-image-2", types: "image_generation,llm" },
{ model_id: "cohere-rerank-v4.0", types: "rerank" },
{ model_id: "", types: "llm" },
{ types: "llm" },
],
})
expect(models).toEqual(["claude-sonnet-4-5-20250929", "gpt-5.1"])
})
it("returns an empty list for malformed payloads", () => {
expect(extractAihubmixModelIds({ data: null })).toEqual([])
expect(extractAihubmixModelIds({})).toEqual([])
expect(extractAihubmixModelIds(null)).toEqual([])
})
})
describe("resolveBaseURL", () => {
const SERVER_BASE_URL = "https://server-proxy.example.com"
@@ -256,6 +282,70 @@ vi.mock("@ai-sdk/deepseek", () => {
return { createDeepSeek: mockCreateDeepSeek, deepseek: mockDeepseek }
})
vi.mock("@aihubmix/ai-sdk-provider", () => {
const mockModel = { modelId: "test-model" }
const mockProviderFn = vi.fn(() => mockModel)
const mockCreateAihubmix = vi.fn(() => mockProviderFn)
const mockAihubmix = vi.fn(() => mockModel)
return { aihubmix: mockAihubmix, createAihubmix: mockCreateAihubmix }
})
describe("AIHubMix provider", () => {
let createAihubmixMock: ReturnType<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {}
beforeEach(async () => {
savedEnv.AIHUBMIX_API_KEY = process.env.AIHUBMIX_API_KEY
savedEnv.AIHUBMIX_BASE_URL = process.env.AIHUBMIX_BASE_URL
delete process.env.AIHUBMIX_BASE_URL
const mod = await import("@aihubmix/ai-sdk-provider")
createAihubmixMock = mod.createAihubmix as ReturnType<typeof vi.fn>
createAihubmixMock.mockClear()
})
afterEach(() => {
process.env.AIHUBMIX_API_KEY = savedEnv.AIHUBMIX_API_KEY
process.env.AIHUBMIX_BASE_URL = savedEnv.AIHUBMIX_BASE_URL
})
it("uses AIHUBMIX_API_KEY for server configured AIHubMix", () => {
process.env.AIHUBMIX_API_KEY = "server-aihubmix-key"
getAIModel({
provider: "aihubmix",
modelId: "claude-sonnet-4-5-20250929",
})
expect(createAihubmixMock).toHaveBeenCalledWith({
apiKey: "server-aihubmix-key",
appCode: "MSBS9675",
})
})
it("uses client BYOK API key for AIHubMix", () => {
getAIModel({
provider: "aihubmix",
apiKey: "client-aihubmix-key",
modelId: "gpt-5.1",
})
expect(createAihubmixMock).toHaveBeenCalledWith({
apiKey: "client-aihubmix-key",
appCode: "MSBS9675",
})
})
it("recognizes AIHubMix standard endpoints", () => {
expect(isAihubmixStandardBaseURL(undefined)).toBe(true)
expect(isAihubmixStandardBaseURL("https://aihubmix.com")).toBe(true)
expect(isAihubmixStandardBaseURL("https://aihubmix.com/v1/")).toBe(true)
expect(isAihubmixStandardBaseURL("https://proxy.example.com/v1")).toBe(
false,
)
})
})
describe("Kimi provider uses createDeepSeek for reasoning_content support", () => {
let createDeepSeekMock: ReturnType<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {}