Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1101,14 +1101,19 @@ OmniRoute stands on the shoulders of giants. It started as a fork of **[9router]
<table>
<tr>
<td align="center" width="150">
<a href="https://www.kimi.com/code?aff=omniroute"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/kimi-color.svg" width="64" alt="Kimi (Moonshot AI)"/></a>
<a href="https://www.kimi.com/code?aff=omniroute">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="public/providers/kimi-logomark-dark.svg">
<img src="public/providers/kimi-logomark-light.svg" width="64" alt="Kimi (Moonshot AI)"/>
</picture>
</a>
<br/><b>Kimi</b><br/><sub>Moonshot AI</sub><br/><br/>
<img src="https://img.shields.io/badge/Official_Supporter-1783FF?style=flat-square" alt="Official Supporter"/>
</td>
<td>
Thanks to <b>Kimi (Moonshot AI)</b> for sponsoring this project! Kimi is the AI lab behind the open-weight K2 and K3 model families — <b>Kimi K3</b> delivers a 1M-token context window, native vision and frontier-level coding at a fraction of closed-model prices, and works out of the box with Claude Code, Codex and every coding tool OmniRoute serves.
<br/><br/>
<b>Where the sponsorship goes:</b> Kimi's API credits power OmniRoute's AI-validated release pipeline — the <i>merge validation powered by Kimi K3</i> stage that reviews every pull request before it ships — plus day-to-day feature development. First-class Kimi support ships on both rails: the direct <a href="https://platform.kimi.ai?aff=omniroute">Moonshot API</a> (<code>kimi-k3</code>) and the <a href="https://www.kimi.com/code?aff=omniroute">Kimi Code coding plan</a> (OAuth and API key). OmniRoute is also the first Brazilian open-source project in Kimi's support program. <a href="https://platform.kimi.ai?aff=omniroute"><b>Get a Moonshot API key →</b></a>
<b>Where the sponsorship goes:</b> Kimi's API credits power OmniRoute's AI-validated release pipeline — the <i>merge validation powered by Kimi K3</i> stage that reviews every pull request before it ships — plus day-to-day feature development. First-class Kimi support ships on both rails: the direct <a href="https://platform.kimi.ai?aff=omniroute">Kimi API</a> (<code>kimi-k3</code>) and the <a href="https://www.kimi.com/code?aff=omniroute">Kimi Code coding plan</a> (OAuth and API key). OmniRoute is also the first Brazilian open-source project in Kimi's support program. <a href="https://platform.kimi.ai?aff=omniroute"><b>Get a Kimi API key →</b></a>
</td>
</tr>
</table>
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ Models:

#### Kimi K2 ($9/month flat)

1. Subscribe: [Moonshot AI](https://platform.moonshot.ai)
1. Subscribe: [Moonshot AI](https://platform.kimi.ai?aff=omniroute)
2. Get API key → Dashboard → Add API Key

**Use:** `kimi/kimi-k2.5` — **Pro Tip:** Fixed $9/month for 10M tokens = $0.90/1M effective cost!
Expand Down
10 changes: 5 additions & 5 deletions docs/reference/PROVIDER_REFERENCE.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions public/providers/kimi-logomark-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions public/providers/kimi-logomark-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
96 changes: 96 additions & 0 deletions src/app/(dashboard)/dashboard/KimiSponsorBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"use client";

import { useState } from "react";
import { useTranslations } from "next-intl";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { APP_CONFIG } from "@/shared/constants/appConfig";
import { shouldShowKimiSponsorBanner } from "./kimiSponsorBanner";

// Official Kimi partnership tracking link — keep in sync with README.md's
// Sponsors section and the aff links wired in the providers onboarding UI
// (ProviderPageHeader.tsx).
const KIMI_CODING_AFF_URL = "https://www.kimi.com/code?aff=omniroute";

// Versioned dismissal key — bump the suffix (e.g. `-v2`) if the banner's
// offer/copy ever changes materially enough to warrant re-showing it to
// users who already dismissed the previous version.
const DISMISS_STORAGE_KEY = "omniroute-kimi-sponsor-banner-dismissed-v1";

function isNotDismissed(): boolean {
try {
return !localStorage.getItem(DISMISS_STORAGE_KEY);
} catch {
return true;
}
}

/**
* Dismissable banner announcing the Kimi (Moonshot AI) official OmniRoute
* partnership on the dashboard home page. Self-contained: reads the app's own
* version (APP_CONFIG.version) to decide whether it is still inside the
* agreed display window (see kimiSponsorBanner.ts) and persists dismissal via
* localStorage, mirroring RiskNoticeBanner's lazy-useState pattern. The
* logomark reuses <ProviderIcon providerId="moonshot" .../> so it stays
* theme-aware for free via the THEMED_SVGS wiring in ProviderIcon.tsx.
*/
export default function KimiSponsorBanner() {
const t = useTranslations("kimiSponsorBanner");
const [visible, setVisible] = useState<boolean>(isNotDismissed);

if (!visible || !shouldShowKimiSponsorBanner(APP_CONFIG.version)) {
return null;
}

const dismiss = () => {
try {
localStorage.setItem(DISMISS_STORAGE_KEY, "true");
} catch {
// ignore — worst case the banner reappears next visit
}
setVisible(false);
};

return (
<div
role="complementary"
aria-label={t("title")}
className="mb-4 flex flex-col gap-3 rounded-lg border border-[#1783FF]/30 bg-[#1783FF]/5 px-4 py-3 dark:bg-[#1783FF]/10 sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex min-w-0 items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-[#1783FF]/10">
<ProviderIcon providerId="moonshot" size={24} type="color" />
</div>
<div className="min-w-0">
<p className="text-sm font-semibold text-text-main">{t("title")}</p>
<p className="mt-0.5 text-xs text-text-muted">{t("description")}</p>
</div>
</div>

<div className="flex shrink-0 items-center gap-3 self-end sm:self-auto">
<div className="flex flex-col items-end gap-0.5">
<a
href={KIMI_CODING_AFF_URL}
target="_blank"
rel="noopener noreferrer"
title={t("partnerLinkNote")}
className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-lg bg-[#1783FF] px-3 py-1.5 text-xs font-semibold text-white transition-colors hover:brightness-110"
>
{t("cta")}
<span className="material-symbols-outlined text-[14px]" aria-hidden="true">
open_in_new
</span>
</a>
<span className="text-[9px] text-text-muted/70">{t("partnerLinkNote")}</span>
</div>
<button
type="button"
onClick={dismiss}
aria-label={t("dismissAriaLabel")}
className="text-text-muted transition-colors hover:text-text-main"
>
<span className="material-symbols-outlined text-[18px]">close</span>
</button>
</div>
</div>
);
}
54 changes: 54 additions & 0 deletions src/app/(dashboard)/dashboard/combos/KimiComboPresetCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import { useTranslations } from "next-intl";
import { Card, Button } from "@/shared/components";

/**
* Kimi Coding combo preset (2026-07 partnership) — card UI. See
* kimiComboPreset.ts for the preset payload + why this exists instead of a
* generic template picker (there isn't one that can pin named providers).
* Styled with the Kimi accent, offered next to AutoComboCatalog on the
* combos page. POSTs via the exact same handleCreate() path as every other
* combo (mirrors handleDuplicate's directness — no separate confirmation
* step).
*/
interface KimiComboPresetCardProps {
/** True when a combo named KIMI_CODING_PRESET_NAME already exists — hides the card. */
alreadyCreated: boolean;
creating: boolean;
onCreate: () => void;
}

export default function KimiComboPresetCard({
alreadyCreated,
creating,
onCreate,
}: KimiComboPresetCardProps) {
const t = useTranslations("combos");

if (alreadyCreated) return null;

return (
<Card
padding="sm"
className="border-2 border-[#1783FF]/70 bg-[#1783FF]/[0.04] shadow-[0_2px_10px_-4px_rgba(23,131,255,0.45)]"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex min-w-0 items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-[#1783FF]/10">
<span className="material-symbols-outlined text-[20px] text-[#1067CC] dark:text-[#7CB8FF]">
bolt
</span>
</div>
<div className="min-w-0">
<p className="text-sm font-semibold text-text-main">{t("kimiPresetTitle")}</p>
<p className="mt-0.5 text-xs text-text-muted">{t("kimiPresetDescription")}</p>
</div>
</div>
<Button size="sm" icon="add" loading={creating} onClick={onCreate} className="shrink-0">
{t("kimiPresetCta")}
</Button>
</div>
</Card>
);
}
45 changes: 45 additions & 0 deletions src/app/(dashboard)/dashboard/combos/kimiComboPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Kimi Coding combo preset — pure data, split out of KimiComboPresetCard.tsx
// so it can be unit-tested with node:test (no DOM/next-intl needed), mirroring
// kimiSponsorBanner.ts.
//
// There is no generic preset/template picker in the combo builder today —
// AUTO_COMBO_TEMPLATES (src/domain/assessment/types.ts, surfaced by
// AutoComboCatalog.tsx) is a different mechanism: category/tier-scored
// zero-config routing, not a named provider+model pin, so it cannot express
// "kimi-k3 primary, kimi-coding/kimi-web fallback". KimiComboPresetCard.tsx is
// the minimal idiomatic addition instead: a one-click "create" card that POSTs
// this exact payload via the same handleCreate() path as every other combo.
//
// Model ids differ per provider even for the "same" underlying model:
// - moonshot (Moonshot API): "kimi-k3"
// - kimi-coding (OAuth coding plan): "k3"
// - kimi-web (web session): "k3"
// See open-sse/config/providers/registry/{moonshot,kimi/coding,kimi/web}.
export const KIMI_CODING_PRESET_NAME = "Kimi Coding";

export interface KimiComboPresetModel {
provider: string;
model: string;
weight: number;
}

export interface KimiComboPreset {
name: string;
strategy: "priority";
models: KimiComboPresetModel[];
}

export const KIMI_CODING_PRESET: KimiComboPreset = {
name: KIMI_CODING_PRESET_NAME,
strategy: "priority",
models: [
{ provider: "moonshot", model: "kimi-k3", weight: 100 },
{ provider: "kimi-coding", model: "k3", weight: 100 },
{ provider: "kimi-web", model: "k3", weight: 100 },
],
};

/** True when `combos` (as returned by GET /api/combos) already has the Kimi Coding preset. */
export function hasKimiCodingPreset(combos: Array<{ name?: string | null }>): boolean {
return combos.some((combo) => combo?.name === KIMI_CODING_PRESET_NAME);
}
21 changes: 21 additions & 0 deletions src/app/(dashboard)/dashboard/combos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import {
} from "@/lib/combos/builderDraft";
import { normalizeComboConfigMode } from "@/shared/constants/comboConfigMode";
import AutoComboCatalog from "./AutoComboCatalog";
import KimiComboPresetCard from "./KimiComboPresetCard";
import { KIMI_CODING_PRESET, hasKimiCodingPreset } from "./kimiComboPreset";
import BuilderIntelligentStep from "./BuilderIntelligentStep";
import IntelligentComboPanel from "./IntelligentComboPanel";
import {
Expand Down Expand Up @@ -688,6 +690,7 @@ export default function CombosPage() {
const [providerNodes, setProviderNodes] = useState([]);
const [showUsageGuide, setShowUsageGuide] = useState(true);
const [recentlyCreatedCombo, setRecentlyCreatedCombo] = useState("");
const [creatingKimiPreset, setCreatingKimiPreset] = useState(false);
const [comboDragIndex, setComboDragIndex] = useState(null);
const [comboDragOverIndex, setComboDragOverIndex] = useState(null);
const [savingComboOrder, setSavingComboOrder] = useState(false);
Expand Down Expand Up @@ -858,6 +861,18 @@ export default function CombosPage() {
await handleCreate(data);
};

// Kimi Coding preset (2026-07 partnership) — one-click create, mirrors
// handleDuplicate's directness (no separate confirmation modal). See
// KimiComboPresetCard.tsx for why this bypasses the combo builder wizard.
const handleCreateKimiPreset = async () => {
setCreatingKimiPreset(true);
try {
await handleCreate(KIMI_CODING_PRESET);
} finally {
setCreatingKimiPreset(false);
}
};

const handleTestCombo = async (combo) => {
setTestingCombo(combo.name);
setTestResults(null);
Expand Down Expand Up @@ -1040,6 +1055,12 @@ export default function CombosPage() {

<AutoComboCatalog />

<KimiComboPresetCard
alreadyCreated={hasKimiCodingPreset(combos)}
creating={creatingKimiPreset}
onCreate={handleCreateKimiPreset}
/>

{showUsageGuide && (
<ComboUsageGuide
onHide={() => setShowUsageGuide(false)}
Expand Down
31 changes: 31 additions & 0 deletions src/app/(dashboard)/dashboard/kimiSponsorBanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Kimi (Moonshot AI) official-partnership sponsor banner — version gate.
//
// Pure logic split out of KimiSponsorBanner.tsx so the gate can be unit-tested
// with node:test (no DOM/next-intl needed), mirroring homeAppearance.ts.

import { isNewer, normalizeVersion } from "@/lib/system/versionCheck";

/**
* Last app version that still shows the Kimi sponsor banner (inclusive).
*
* Kimi partnership: banner runs v3.8.49 through v3.8.60 (10 official releases
* counted from v3.8.50), capped at 3 months from 2026-07-21.
*/
export const KIMI_SPONSOR_BANNER_THROUGH_VERSION = "3.8.60";

/**
* Whether the Kimi sponsor banner should render for the given running app
* version. True while `currentVersion <= KIMI_SPONSOR_BANNER_THROUGH_VERSION`
* (semver comparison, reusing the same `isNewer`/`normalizeVersion` helpers as
* the "Update Available" banner). Fails safe (hides the banner) when the
* version string cannot be parsed at all, rather than defaulting to "always
* show" for a value like the `"unknown"` sentinel `getCurrentVersion()` can
* return if `package.json` cannot be read.
*/
export function shouldShowKimiSponsorBanner(currentVersion: string | null | undefined): boolean {
if (typeof currentVersion !== "string") return false;
if (!normalizeVersion(currentVersion)) return false;
// isNewer(a, b) is true iff a > b — so isNewer(current, THROUGH) is true
// exactly when current is past the sunset version. Negate for "<=".
return !isNewer(currentVersion, KIMI_SPONSOR_BANNER_THROUGH_VERSION);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
// Phase 1t.1 extraction — Issue #3501
import Link from "next/link";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { getHeaderIconProviderId } from "../providerPageHelpers";
import { getHeaderIconProviderId, providerText } from "../providerPageHelpers";
import type { ProviderMessageTranslator } from "../providerPageHelpers";
import { isKimiPartnerProviderId } from "../../featuredProviders";

interface ProviderInfo {
id: string;
Expand Down Expand Up @@ -37,6 +38,19 @@ export default function ProviderPageHeader({
onOpenTutorial,
t,
}: ProviderPageHeaderProps) {
// Kimi (Moonshot AI) official-partnership aff links (2026-07): the header
// website link doubles as the CTA for kimi-coding/kimi-web/moonshot's
// tracking links (see website field in oauth.ts / web-cookie.ts /
// apikey/regional.ts) — flag it with a discreet "Partner link" note so it
// reads as a monetized link, not just "visit provider website" like every
// other card. UI-only — never affects routing/fallback (featuredProviders.ts).
const isKimiPartnerLink = isKimiPartnerProviderId(providerInfo.id);
const kimiPartnerLinkNote = providerText(
t,
"kimiPartnerLinkNote",
"Partner link — supports OmniRoute at no extra cost to you"
);

return (
<div>
<Link
Expand Down Expand Up @@ -74,6 +88,10 @@ export default function ProviderPageHeader({
rel="noopener noreferrer"
className="text-3xl font-semibold tracking-tight hover:underline inline-flex items-center gap-2"
style={{ color: providerInfo.color }}
title={isKimiPartnerLink ? kimiPartnerLinkNote : undefined}
aria-label={
isKimiPartnerLink ? `${providerInfo.name} — ${kimiPartnerLinkNote}` : undefined
}
>
{providerInfo.name}
<span className="material-symbols-outlined text-lg opacity-60">open_in_new</span>
Expand All @@ -85,6 +103,11 @@ export default function ProviderPageHeader({
<p className="text-text-muted">
{t("connectionCountLabel", { count: connectionsCount })}
</p>
{isKimiPartnerLink && providerInfo.website && (
<span className="text-[10px] font-medium uppercase tracking-wide text-text-muted/70">
{kimiPartnerLinkNote}
</span>
)}
{providerId === "adapta-web" && (
<button
onClick={onOpenTutorial}
Expand Down
2 changes: 2 additions & 0 deletions src/app/(dashboard)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getMachineId } from "@/shared/utils/machine";
import { getSettings } from "@/lib/localDb";
import HomePageClient from "../dashboard/HomePageClient";
import BootstrapBanner from "../dashboard/BootstrapBanner";
import KimiSponsorBanner from "../dashboard/KimiSponsorBanner";

export const dynamic = "force-dynamic";

Expand All @@ -16,6 +17,7 @@ export default async function HomePage() {
return (
<>
{isBootstrapped && <BootstrapBanner />}
<KimiSponsorBanner />
<HomePageClient machineId={machineId} />
</>
);
Expand Down
Loading
Loading