Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ describe("provider identifiers", () => {
providerIdentifiers.opencodeGo,
providerIdentifiers.kenari,
providerIdentifiers.kimiCode,
providerIdentifiers.friendli,
])
expect(localProviders).toEqual([providerIdentifiers.ollama, providerIdentifiers.lmstudio])
expect(internalProviders).toEqual([providerIdentifiers.vscodeLm])
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const dynamicProviders = [
providerIdentifiers.opencodeGo,
providerIdentifiers.kenari,
providerIdentifiers.kimiCode,
providerIdentifiers.friendli,
] as const

export type DynamicProvider = (typeof dynamicProviders)[number]
Expand Down
8 changes: 6 additions & 2 deletions packages/types/src/providers/friendli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ export type FriendliModelId =

export const friendliDefaultModelId: FriendliModelId = "zai-org/GLM-5.2"

// Static fallback for the Friendli provider. Used as a fallback when dynamic
// models cannot be fetched (cold start, network errors, API lag), in tests,
// and in the webview's MODELS_BY_PROVIDER fallback. The provider itself fetches
// the live list from https://api.friendli.ai/serverless/v1/models at runtime.
// Pricing sourced from https://friendli.ai/api/public/model-apis (per 1M tokens).
export const friendliModels = {
export const friendliModels: Record<string, ModelInfo> = {
"zai-org/GLM-5.2": {
maxTokens: 131_072,
contextWindow: 1_000_000,
Expand Down Expand Up @@ -64,4 +68,4 @@ export const friendliModels = {
description:
"MiniMax M2.5 is a high-performance language model with a 204.8K context window, optimized for long-context understanding and generation tasks, served via Friendli Model APIs.",
},
} as const satisfies Record<string, ModelInfo>
}
104 changes: 103 additions & 1 deletion src/api/providers/__tests__/friendli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { FriendliHandler } from "../friendli"
import { asyncStreamFrom, collectStream } from "../../../test-utils/stream"

// Create mock functions
const mockCreate = vi.fn()
const { mockCreate, mockGetModels } = vi.hoisted(() => ({
mockCreate: vi.fn(),
mockGetModels: vi.fn(),
}))

// Mock OpenAI module
vi.mock("openai", () => ({
Expand All @@ -26,11 +29,18 @@ vi.mock("openai", () => ({
}),
}))

// Mock modelCache so we can control dynamic model loading
vi.mock("../fetchers/modelCache", () => ({
getModels: mockGetModels,
}))

describe("FriendliHandler", () => {
let handler: FriendliHandler

beforeEach(() => {
vi.clearAllMocks()
// By default, dynamic model fetch resolves to empty (static models win)
mockGetModels.mockResolvedValue({})
// Set up default mock implementation
mockCreate.mockImplementation(async () =>
asyncStreamFrom([
Expand Down Expand Up @@ -540,3 +550,95 @@ describe("FriendliHandler — Friendli-specific reasoning params", () => {
expect(callArgs.include_reasoning).toBe(true)
})
})

describe("FriendliHandler — dynamic model loading", () => {
beforeEach(() => {
vi.clearAllMocks()
mockCreate.mockImplementation(async () => asyncStreamFrom([]))
})

it("preserves a dynamic-only model id during the initial load window", () => {
// mockGetModels never resolves — simulates an in-flight fetch
mockGetModels.mockReturnValue(new Promise(() => {}))

const handler = new FriendliHandler({
apiModelId: "friendli-only/future-model",
friendliApiKey: "test-key",
})

// "friendli-only/future-model" is not in static friendliModels, but
// because dynamicModelsLoaded is still false the handler keeps the
// requested id and falls back to the default model's metadata.
const model = handler.getModel()
expect(model.id).toBe("friendli-only/future-model")
expect(model.info).toEqual(friendliModels[friendliDefaultModelId])
})

it("falls back to default model after load completes and id is not in dynamic set", async () => {
// Dynamic fetch resolves to empty — no models
mockGetModels.mockResolvedValue({})

const handler = new FriendliHandler({
apiModelId: "friendli-only/future-model",
friendliApiKey: "test-key",
})

// Wait for the dynamic fetch to settle
await vi.waitFor(() => {
expect((handler as unknown as Record<string, unknown>)["dynamicModelsLoaded"]).toBe(true)
})

// After load, the dynamic-only id is not found — falls back to default
const model = handler.getModel()
expect(model.id).toBe(friendliDefaultModelId)
})

it("uses dynamic model info when available", async () => {
const dynamicModel = {
"friendli-only/future-model": {
maxTokens: 8192,
contextWindow: 100000,
supportsImages: false,
supportsPromptCache: false,
description: "A dynamic-only model",
},
}
mockGetModels.mockResolvedValue(dynamicModel)

const handler = new FriendliHandler({
apiModelId: "friendli-only/future-model",
friendliApiKey: "test-key",
})

await vi.waitFor(() => {
expect((handler as unknown as Record<string, unknown>)["dynamicModelsLoaded"]).toBe(true)
})

const model = handler.getModel()
expect(model.id).toBe("friendli-only/future-model")
expect(model.info).toEqual(
expect.objectContaining({
maxTokens: 8192,
contextWindow: 100000,
description: "A dynamic-only model",
}),
)
})

it("sets dynamicModelsLoaded even when getModels rejects", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
mockGetModels.mockRejectedValue(new Error("Network error"))

const handler = new FriendliHandler({
friendliApiKey: "test-key",
})

await vi.waitFor(() => {
expect((handler as unknown as Record<string, unknown>)["dynamicModelsLoaded"]).toBe(true)
})

// Falls back to default model
expect(handler.getModel().id).toBe(friendliDefaultModelId)
consoleErrorSpy.mockRestore()
})
})
Loading
Loading