feat: add Kimi/Moonshot partnership branding with feature flag

## Summary
- Add `VITE_PUBLIC_KIMI_LAUNCH` feature flag controlling Kimi partnership branding
- BrowserOS provider card shows "Powered by Kimi K2.5 from Moonshot AI" badge and "Extended usage limits for the next 2 weeks!" when flag is on
- Moonshot/Kimi highlighted as "Recommended" in provider templates
- LLM Hub defaults to Kimi, ChatGPT, Claude, Gemini (with legacy defaults migration)
- Kimi hub row shows "Powered by Moonshot AI" flare
- Model selector locked to kimi-k2.5
- "How to get a Kimi API key" link in provider dialog
- Moonshot provider fully integrated across frontend and backend
This commit is contained in:
Felarof
2026-03-04 17:55:40 -08:00
committed by GitHub
parent 3969660906
commit 609341a445
22 changed files with 187 additions and 43 deletions

View File

@@ -15,6 +15,9 @@ VITE_PUBLIC_SENTRY_DSN=
# BrowserOS API URL
VITE_PUBLIC_BROWSEROS_API=https://api.browseros.com
# Launch feature flags
VITE_PUBLIC_KIMI_LAUNCH=false
# GraphQL Schema Path (optional — falls back to schema/schema.graphql)
GRAPHQL_SCHEMA_PATH=

View File

@@ -45,6 +45,7 @@ import { track } from '@/lib/metrics/track'
import { getModelContextLength, getModelOptions } from './models'
const providerTypeEnum = z.enum([
'moonshot',
'anthropic',
'openai',
'openai-compatible',
@@ -413,6 +414,12 @@ export const NewProviderDialog: FC<NewProviderDialogProps> = ({
const providerTemplate = getProviderTemplate(watchedType as ProviderType)
const setupGuideUrl = providerTemplate?.setupGuideUrl
const providerName = providerTemplate?.name
const setupGuideText =
watchedType === 'moonshot'
? 'How to get a Kimi API key'
: providerName
? `${providerName} setup guide`
: 'Provider setup guide'
const handleSetupGuideClick = (e: React.MouseEvent) => {
e.preventDefault()
@@ -596,7 +603,7 @@ export const NewProviderDialog: FC<NewProviderDialogProps> = ({
className="inline-flex cursor-pointer items-center gap-1 text-primary hover:underline"
>
<ExternalLink className="h-3 w-3" />
{providerName} setup guide
{setupGuideText}
</a>
)}
</FormDescription>

View File

@@ -2,6 +2,7 @@ import { Check, Loader2, Trash2 } from 'lucide-react'
import type { FC } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { useKimiLaunch } from '@/lib/feature-flags/useKimiLaunch'
import { BrowserOSIcon, ProviderIcon } from '@/lib/llm-providers/providerIcons'
import type { LlmProviderConfig } from '@/lib/llm-providers/types'
import { cn } from '@/lib/utils'
@@ -29,6 +30,7 @@ export const ProviderCard: FC<ProviderCardProps> = ({
isTesting = false,
}) => {
const inputId = `provider-${provider.id}`
const kimiLaunch = useKimiLaunch()
return (
<label
@@ -77,21 +79,30 @@ export const ProviderCard: FC<ProviderCardProps> = ({
</Badge>
)}
</div>
{isBuiltIn && provider.type === 'browseros' && kimiLaunch && (
<span className="mb-1 inline-block rounded-full border border-orange-300/60 bg-orange-100/70 px-3 py-0.5 font-semibold text-orange-700 text-xs dark:border-orange-400/40 dark:bg-orange-500/15 dark:text-orange-300">
Powered by Kimi K2.5 from Moonshot AI
</span>
)}
<p className="truncate text-muted-foreground text-sm">
{isBuiltIn ? (
<>
BrowserOS-hosted model with strict rate limits.{' '}
<a
href="https://docs.browseros.com/features/bring-your-own-llm"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
Bring your own key
</a>{' '}
for better performance.
</>
kimiLaunch ? (
'Extended usage limits for the next 2 weeks!'
) : (
<>
BrowserOS-hosted model with strict rate limits.{' '}
<a
href="https://docs.browseros.com/features/bring-your-own-llm"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
Bring your own key
</a>{' '}
for better performance.
</>
)
) : (
`${provider.modelId}${provider.baseUrl}`
)}

View File

@@ -2,29 +2,54 @@ import type { FC } from 'react'
import { Badge } from '@/components/ui/badge'
import { ProviderIcon } from '@/lib/llm-providers/providerIcons'
import type { ProviderTemplate } from '@/lib/llm-providers/providerTemplates'
import { cn } from '@/lib/utils'
interface ProviderTemplateCardProps {
template: ProviderTemplate
highlighted?: boolean
onUseTemplate: (template: ProviderTemplate) => void
}
export const ProviderTemplateCard: FC<ProviderTemplateCardProps> = ({
template,
highlighted = false,
onUseTemplate,
}) => {
return (
<button
type="button"
onClick={() => onUseTemplate(template)}
className="group flex w-full items-center justify-between rounded-lg border border-border bg-background p-4 text-left transition-all hover:border-[var(--accent-orange)] hover:shadow-md"
className={cn(
'group flex w-full items-center gap-3 rounded-lg border bg-background p-4 text-left transition-all hover:border-[var(--accent-orange)] hover:shadow-md',
highlighted
? 'border-orange-300/80 bg-orange-50/30 shadow-sm ring-1 ring-orange-300/45 dark:bg-orange-500/5'
: 'border-border',
)}
>
<div className="flex items-center gap-3 text-accent-orange/70 transition-colors group-hover:text-accent-orange">
<ProviderIcon type={template.id} size={28} />
<span className="font-medium text-foreground">{template.name}</span>
<div className="flex min-w-0 flex-1 items-center gap-3">
<ProviderIcon
type={template.id}
size={28}
className="shrink-0 text-accent-orange/70 transition-colors group-hover:text-accent-orange"
/>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium text-foreground">{template.name}</span>
{highlighted && (
<span className="rounded-full border border-orange-300/60 bg-orange-100/70 px-2 py-0.5 font-semibold text-[10px] text-orange-700 dark:border-orange-400/40 dark:bg-orange-500/15 dark:text-orange-300">
Recommended
</span>
)}
</div>
</div>
</div>
<Badge
variant="outline"
className="rounded-md px-3 py-1 transition-colors group-hover:border-[var(--accent-orange)] group-hover:text-[var(--accent-orange)]"
className={cn(
'shrink-0 rounded-md px-3 py-1 transition-colors group-hover:border-[var(--accent-orange)] group-hover:text-[var(--accent-orange)]',
highlighted &&
'border-[var(--accent-orange)] bg-[var(--accent-orange)]/5 text-[var(--accent-orange)]',
)}
>
USE
</Badge>

View File

@@ -54,6 +54,7 @@ export const ProviderTemplatesSection: FC<ProviderTemplatesSectionProps> = ({
<ProviderTemplateCard
key={template.id}
template={template}
highlighted={template.id === 'moonshot'}
onUseTemplate={onUseTemplate}
/>
))}

View File

@@ -22,6 +22,7 @@ export interface ModelsData {
lmstudio: ModelInfo[]
bedrock: ModelInfo[]
browseros: ModelInfo[]
moonshot: ModelInfo[]
}
/**
@@ -29,6 +30,7 @@ export interface ModelsData {
* Based on: https://github.com/browseros-ai/BrowserOS-agent/blob/main/src/options/data/models.ts
*/
export const MODELS_DATA: ModelsData = {
moonshot: [{ modelId: 'kimi-k2.5', contextLength: 128000 }],
anthropic: [
{ modelId: 'claude-opus-4-5-20251101', contextLength: 200000 },
{ modelId: 'claude-haiku-4-5-20251001', contextLength: 200000 },

View File

@@ -2,6 +2,7 @@ import { Globe2, Trash2 } from 'lucide-react'
import type { FC } from 'react'
import { useMemo } from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { getFaviconUrl, type LlmHubProvider } from './models'
interface HubProviderRowProps {
@@ -18,9 +19,19 @@ export const HubProviderRow: FC<HubProviderRowProps> = ({
onDelete,
}) => {
const iconUrl = useMemo(() => getFaviconUrl(provider.url), [provider.url])
const normalizedName = provider.name.trim().toLowerCase()
const normalizedUrl = provider.url.trim().toLowerCase()
const isKimi = normalizedName === 'kimi' || normalizedUrl.includes('kimi.com')
const showKimiFlare = isKimi
return (
<div className="group flex w-full items-center gap-4 rounded-xl border border-border bg-card p-4 transition-all hover:border-[var(--accent-orange)] hover:shadow-md">
<div
className={cn(
'group flex w-full items-center gap-4 rounded-xl border border-border bg-card p-4 transition-all hover:border-[var(--accent-orange)] hover:shadow-md',
showKimiFlare &&
'border-orange-300/80 bg-orange-50/20 shadow-sm ring-1 ring-orange-300/45 dark:bg-orange-500/5',
)}
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted">
{iconUrl ? (
<img
@@ -34,9 +45,19 @@ export const HubProviderRow: FC<HubProviderRowProps> = ({
</div>
<div className="min-w-0 flex-1">
<span className="mb-0.5 block truncate font-semibold">
{provider.name}
</span>
<div className="mb-0.5 flex items-center gap-2">
<span className="block truncate font-semibold">{provider.name}</span>
{showKimiFlare && (
<div className="flex flex-wrap items-center gap-1">
<span className="rounded-full border border-orange-300/60 bg-orange-100/70 px-2 py-0.5 font-semibold text-[11px] text-orange-700 dark:border-orange-400/40 dark:bg-orange-500/15 dark:text-orange-300">
Recommended
</span>
<span className="rounded-full border border-orange-300/60 bg-orange-100/60 px-2.5 py-0.5 font-medium text-orange-700 text-xs dark:border-orange-400/40 dark:bg-orange-500/15 dark:text-orange-300">
Powered by Moonshot AI
</span>
</div>
)}
</div>
<p className="truncate text-muted-foreground/70 text-xs">
{provider.url}
</p>

View File

@@ -34,7 +34,7 @@ export const HubProvidersList: FC<HubProvidersListProps> = ({
return (
<div className="rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="mb-4 flex items-center justify-between">
<h3 className="font-medium">Configured Providers</h3>
<h3 className="font-medium">Configured AI Providers</h3>
<Button
variant="outline"
size="sm"
@@ -57,7 +57,7 @@ export const HubProvidersList: FC<HubProvidersListProps> = ({
return (
<div className="rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="mb-4 flex items-center justify-between">
<h3 className="font-medium">Configured Providers</h3>
<h3 className="font-medium">Configured AI Providers</h3>
<Button
variant="outline"
size="sm"

View File

@@ -1,5 +1,4 @@
export {
DEFAULT_PROVIDERS,
getFaviconUrl,
type LlmHubProvider,
} from '@/lib/llm-hub/storage'

View File

@@ -6,6 +6,7 @@ const EnvSchema = z.object({
VITE_PUBLIC_POSTHOG_HOST: z.string().optional(),
VITE_PUBLIC_SENTRY_DSN: z.string().optional(),
VITE_PUBLIC_BROWSEROS_API: z.string().optional(),
VITE_PUBLIC_KIMI_LAUNCH: z.string().optional(),
PROD: z.boolean(),
})

View File

@@ -0,0 +1,14 @@
import { env } from '@/lib/env'
const ENABLED_VALUES = new Set(['1', 'true', 'yes', 'on'])
function parseKimiLaunchFlag(value: string | undefined): boolean {
if (!value) return false
return ENABLED_VALUES.has(value.trim().toLowerCase())
}
const kimiLaunchEnabled = parseKimiLaunchFlag(env.VITE_PUBLIC_KIMI_LAUNCH)
export function isKimiLaunchEnabled(): boolean {
return kimiLaunchEnabled
}

View File

@@ -0,0 +1,5 @@
import { isKimiLaunchEnabled } from './kimi-launch'
export function useKimiLaunch(): boolean {
return isKimiLaunchEnabled()
}

View File

@@ -7,13 +7,17 @@ export interface LlmHubProvider {
url: string
}
export const DEFAULT_PROVIDERS: LlmHubProvider[] = [
{ name: 'ChatGPT', url: 'https://chatgpt.com' },
{ name: 'Claude', url: 'https://claude.ai' },
{ name: 'Grok', url: 'https://grok.com' },
{ name: 'Gemini', url: 'https://gemini.google.com' },
{ name: 'Perplexity', url: 'https://www.perplexity.ai' },
]
const KIMI_PROVIDER: LlmHubProvider = {
name: 'Kimi',
url: 'https://www.kimi.com',
}
function ensureKimiFirst(providers: LlmHubProvider[]): LlmHubProvider[] {
const hasKimi = providers.some(
(p) => p.name === 'Kimi' || p.url.includes('kimi.com'),
)
return hasKimi ? providers : [KIMI_PROVIDER, ...providers]
}
export async function loadProviders(): Promise<LlmHubProvider[]> {
try {
@@ -24,12 +28,12 @@ export async function loadProviders(): Promise<LlmHubProvider[]> {
const providers = (providersPref?.value as LlmHubProvider[]) || []
if (providers.length === 0) {
return DEFAULT_PROVIDERS
return [KIMI_PROVIDER]
}
return providers
return ensureKimiFirst(providers)
} catch {
return DEFAULT_PROVIDERS
return [KIMI_PROVIDER]
}
}

View File

@@ -1,10 +1,5 @@
import { useEffect, useState } from 'react'
import {
DEFAULT_PROVIDERS,
type LlmHubProvider,
loadProviders,
saveProviders,
} from './storage'
import { type LlmHubProvider, loadProviders, saveProviders } from './storage'
/** @public */
export interface UseLlmHubProvidersReturn {
@@ -26,7 +21,7 @@ export function useLlmHubProviders(): UseLlmHubProvidersReturn {
const data = await loadProviders()
setProviders(data)
} catch {
setProviders(DEFAULT_PROVIDERS)
setProviders([])
} finally {
setIsLoading(false)
}

View File

@@ -3,6 +3,7 @@ import {
Azure,
Bedrock,
Gemini,
Kimi,
LmStudio,
Ollama,
OpenAI,
@@ -30,6 +31,7 @@ const providerIconMap: Record<ProviderType, IconComponent | null> = {
lmstudio: LmStudio,
bedrock: Bedrock,
browseros: null,
moonshot: Kimi,
}
interface ProviderIconProps {

View File

@@ -20,6 +20,16 @@ export interface ProviderTemplate {
* @public
*/
export const providerTemplates: ProviderTemplate[] = [
{
id: 'moonshot',
name: 'Moonshot AI',
defaultBaseUrl: 'https://api.moonshot.ai/v1',
defaultModelId: 'kimi-k2.5',
supportsImages: true,
contextWindow: 128000,
apiKeyUrl: 'https://platform.moonshot.ai/console/api-keys',
setupGuideUrl: 'https://platform.moonshot.ai/console/api-keys',
},
{
id: 'openai',
name: 'OpenAI',
@@ -119,6 +129,7 @@ export const providerTemplates: ProviderTemplate[] = [
* @public
*/
export const providerTypeOptions: { value: ProviderType; label: string }[] = [
{ value: 'moonshot', label: 'Moonshot AI' },
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'openai', label: 'OpenAI' },
{ value: 'openai-compatible', label: 'OpenAI Compatible' },
@@ -146,6 +157,7 @@ export const getProviderTemplate = (
* Auto-fills when user selects a provider type
*/
export const DEFAULT_BASE_URLS: Record<ProviderType, string> = {
moonshot: 'https://api.moonshot.ai/v1',
anthropic: 'https://api.anthropic.com/v1',
openai: 'https://api.openai.com/v1',
'openai-compatible': '',

View File

@@ -13,6 +13,7 @@ export type ProviderType =
| 'lmstudio'
| 'bedrock'
| 'browseros'
| 'moonshot'
/**
* LLM Provider configuration

View File

@@ -160,6 +160,18 @@ function createOpenAICompatibleFactory(
})
}
function createMoonshotFactory(
config: VercelAIConfig,
): (modelId: string) => unknown {
if (!config.baseUrl) throw new Error('Moonshot provider requires baseUrl')
if (!config.apiKey) throw new Error('Moonshot provider requires apiKey')
return createOpenAICompatible({
name: 'moonshot',
baseURL: config.baseUrl,
apiKey: config.apiKey,
})
}
const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[AIProvider.ANTHROPIC]: createAnthropicFactory,
[AIProvider.OPENAI]: createOpenAIFactory,
@@ -171,6 +183,7 @@ const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[AIProvider.BEDROCK]: createBedrockFactory,
[AIProvider.BROWSEROS]: createBrowserOSFactory,
[AIProvider.OPENAI_COMPATIBLE]: createOpenAICompatibleFactory,
[AIProvider.MOONSHOT]: createMoonshotFactory,
}
/**

View File

@@ -214,6 +214,7 @@ export enum AIProvider {
BEDROCK = 'bedrock',
BROWSEROS = 'browseros',
OPENAI_COMPATIBLE = 'openai-compatible',
MOONSHOT = 'moonshot',
}
/**

View File

@@ -136,6 +136,18 @@ function createOpenAICompatibleFactory(
})
}
function createMoonshotFactory(
config: ResolvedAgentConfig,
): (modelId: string) => unknown {
if (!config.baseUrl) throw new Error('Moonshot provider requires baseUrl')
if (!config.apiKey) throw new Error('Moonshot provider requires apiKey')
return createOpenAICompatible({
name: 'moonshot',
baseURL: config.baseUrl,
apiKey: config.apiKey,
})
}
const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[LLM_PROVIDERS.ANTHROPIC]: createAnthropicFactory,
[LLM_PROVIDERS.OPENAI]: createOpenAIFactory,
@@ -147,6 +159,7 @@ const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[LLM_PROVIDERS.BEDROCK]: createBedrockFactory,
[LLM_PROVIDERS.BROWSEROS]: createBrowserOSFactory,
[LLM_PROVIDERS.OPENAI_COMPATIBLE]: createOpenAICompatibleFactory,
[LLM_PROVIDERS.MOONSHOT]: createMoonshotFactory,
}
export function createLanguageModel(

View File

@@ -124,6 +124,16 @@ function createOpenAICompatibleModel(config: ResolvedLLMConfig): LanguageModel {
})(config.model)
}
function createMoonshotModel(config: ResolvedLLMConfig): LanguageModel {
if (!config.baseUrl) throw new Error('Moonshot provider requires baseUrl')
if (!config.apiKey) throw new Error('Moonshot provider requires apiKey')
return createOpenAICompatible({
name: 'moonshot',
baseURL: config.baseUrl,
apiKey: config.apiKey,
})(config.model)
}
const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[LLM_PROVIDERS.ANTHROPIC]: createAnthropicModel,
[LLM_PROVIDERS.OPENAI]: createOpenAIModel,
@@ -135,6 +145,7 @@ const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[LLM_PROVIDERS.BEDROCK]: createBedrockModel,
[LLM_PROVIDERS.BROWSEROS]: createBrowserOSModel,
[LLM_PROVIDERS.OPENAI_COMPATIBLE]: createOpenAICompatibleModel,
[LLM_PROVIDERS.MOONSHOT]: createMoonshotModel,
}
export function createLLMProvider(config: ResolvedLLMConfig): LanguageModel {

View File

@@ -23,6 +23,7 @@ export const LLM_PROVIDERS = {
BEDROCK: 'bedrock',
BROWSEROS: 'browseros',
OPENAI_COMPATIBLE: 'openai-compatible',
MOONSHOT: 'moonshot',
} as const
/**
@@ -40,6 +41,7 @@ export const LLMProviderSchema: z.ZodEnum<
'bedrock',
'browseros',
'openai-compatible',
'moonshot',
]
> = z.enum([
LLM_PROVIDERS.ANTHROPIC,
@@ -52,6 +54,7 @@ export const LLMProviderSchema: z.ZodEnum<
LLM_PROVIDERS.BEDROCK,
LLM_PROVIDERS.BROWSEROS,
LLM_PROVIDERS.OPENAI_COMPATIBLE,
LLM_PROVIDERS.MOONSHOT,
])
export type LLMProvider = z.infer<typeof LLMProviderSchema>