Skip to content
Open
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
7 changes: 7 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/config/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Mux ships with curated models kept up to date with the frontier. Use any custom
| Grok Code Fast 1 | xai:grok-code-fast-1 | `grok-code` | |
| DeepSeek V4 Pro | deepseek:deepseek-v4-pro | `deepseek`, `deepseek-pro`, `deepseek-v4`, `deepseek-v4-pro` | |
| DeepSeek V4 Flash | deepseek:deepseek-v4-flash | `deepseek-flash`, `deepseek-v4-flash` | |
| Kimi K3 | moonshotai:kimi-k3 | `kimi`, `k3`, `kimi-k3` | |

{/* END KNOWN_MODELS_TABLE */}

Expand Down
2 changes: 2 additions & 0 deletions docs/config/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Most providers only need an API key. The UI handles validation and shows which p
| **Google** | Gemini Pro, Flash | [aistudio.google.com](https://aistudio.google.com/) |
| **xAI** | Grok | [console.x.ai](https://console.x.ai/) |
| **DeepSeek** | DeepSeek Chat, Reasoner | [platform.deepseek.com](https://platform.deepseek.com/) |
| **Moonshot AI** | Kimi K3 | [platform.moonshot.ai](https://platform.moonshot.ai/) |
| **OpenRouter** | 300+ models | [openrouter.ai](https://openrouter.ai/) |
| **Ollama** | Local models | [ollama.com](https://ollama.com/) (no key needed) |
| **Bedrock** | Claude via AWS | AWS Console |
Expand All @@ -42,6 +43,7 @@ Providers also read from environment variables as fallback:
| xAI | `XAI_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY` |
| DeepSeek | `DEEPSEEK_API_KEY` |
| Moonshot AI | `MOONSHOT_API_KEY` |
| github-copilot | `GITHUB_COPILOT_TOKEN` |
| Bedrock | `AWS_REGION` (credentials via AWS SDK chain) |

Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@

outputHashMode = "recursive";
# Marker used by scripts/update_flake_hash.sh to update this hash in place.
outputHash = "sha256-PKc0nF1lp6yjPlveIhOc/Hp6DwFe3WgtfHyHTDQdUnQ="; # mux-offline-cache-hash
outputHash = "sha256-OzBNhFDq9TWcHNfEGI5SpOVvin9WVzi9SvsG34G267A="; # mux-offline-cache-hash
};

configurePhase = ''
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@ai-sdk/deepseek": "^3.0.7",
"@ai-sdk/google": "^4.0.11",
"@ai-sdk/mcp": "^2.0.10",
"@ai-sdk/moonshotai": "^3.0.15",
"@ai-sdk/openai": "^4.0.11",
"@ai-sdk/openai-compatible": "^3.0.7",
"@ai-sdk/xai": "^4.0.10",
Expand Down
1 change: 1 addition & 0 deletions scripts/gen_docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
google: "Google",
xai: "xAI",
deepseek: "DeepSeek",
moonshotai: "Moonshot AI",
openrouter: "OpenRouter",
bedrock: "Bedrock",
};
Expand Down
4 changes: 4 additions & 0 deletions src/browser/assets/icons/moonshotai.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function installProviderIconSvgMocks() {
"@/browser/assets/icons/openrouter.svg?react",
"@/browser/assets/icons/ollama.svg?react",
"@/browser/assets/icons/deepseek.svg?react",
"@/browser/assets/icons/moonshotai.svg?react",
"@/browser/assets/icons/aws.svg?react",
"@/browser/assets/icons/github.svg?react",
] as const;
Expand Down
2 changes: 2 additions & 0 deletions src/browser/components/ProviderIcon/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import XAIIcon from "@/browser/assets/icons/xai.svg?react";
import OpenRouterIcon from "@/browser/assets/icons/openrouter.svg?react";
import OllamaIcon from "@/browser/assets/icons/ollama.svg?react";
import DeepSeekIcon from "@/browser/assets/icons/deepseek.svg?react";
import MoonshotAIIcon from "@/browser/assets/icons/moonshotai.svg?react";
import AWSIcon from "@/browser/assets/icons/aws.svg?react";
import GitHubIcon from "@/browser/assets/icons/github.svg?react";
import { GatewayIcon } from "@/browser/components/icons/GatewayIcon/GatewayIcon";
Expand All @@ -27,6 +28,7 @@ const PROVIDER_ICONS: Partial<Record<ProviderName, React.FC>> = {
google: GoogleIcon,
xai: XAIIcon,
deepseek: DeepSeekIcon,
moonshotai: MoonshotAIIcon,
openrouter: OpenRouterIcon,
bedrock: AWSIcon,
ollama: OllamaIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ const PROVIDER_KEY_URLS: Partial<Record<ProviderName, string>> = {
google: "https://aistudio.google.com/app/apikey",
xai: "https://console.x.ai/team/default/api-keys",
deepseek: "https://platform.deepseek.com/api_keys",
moonshotai: "https://platform.moonshot.ai/console/api-keys",
openrouter: "https://openrouter.ai/settings/keys",
// bedrock: AWS credential chain, no simple key URL
// ollama: local service, no key needed
Expand Down
2 changes: 1 addition & 1 deletion src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ async function main(): Promise<number> {
config.saveProvidersConfig(providersFromEnv);
} else {
throw new Error(
"No provider credentials found. Configure providers.jsonc or set ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY."
"No provider credentials found. Configure providers.jsonc or set ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / MOONSHOT_API_KEY."
);
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/common/config/schemas/providersConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const MuxGatewayProviderConfigSchema = BaseProviderConfigSchema.extend({

export const GoogleProviderConfigSchema = BaseProviderConfigSchema;
export const DeepSeekProviderConfigSchema = BaseProviderConfigSchema;
export const MoonshotAIProviderConfigSchema = BaseProviderConfigSchema;
export const OllamaProviderConfigSchema = BaseProviderConfigSchema;
export const GitHubCopilotProviderConfigSchema = BaseProviderConfigSchema;

Expand All @@ -81,6 +82,7 @@ export const ProvidersConfigSchema = z
"mux-gateway": MuxGatewayProviderConfigSchema.optional(),
google: GoogleProviderConfigSchema.optional(),
deepseek: DeepSeekProviderConfigSchema.optional(),
moonshotai: MoonshotAIProviderConfigSchema.optional(),
ollama: OllamaProviderConfigSchema.optional(),
"github-copilot": GitHubCopilotProviderConfigSchema.optional(),
})
Expand All @@ -95,6 +97,7 @@ export type XAIProviderConfig = z.infer<typeof XAIProviderConfigSchema>;
export type MuxGatewayProviderConfig = z.infer<typeof MuxGatewayProviderConfigSchema>;
export type GoogleProviderConfig = z.infer<typeof GoogleProviderConfigSchema>;
export type DeepSeekProviderConfig = z.infer<typeof DeepSeekProviderConfigSchema>;
export type MoonshotAIProviderConfig = z.infer<typeof MoonshotAIProviderConfigSchema>;
export type OllamaProviderConfig = z.infer<typeof OllamaProviderConfigSchema>;
export type GitHubCopilotProviderConfig = z.infer<typeof GitHubCopilotProviderConfigSchema>;

Expand Down
14 changes: 11 additions & 3 deletions src/common/constants/knownModels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ describe("Known Models Integration", () => {
for (const [key, model] of Object.entries(KNOWN_MODELS)) {
const modelId = model.providerModelId;

// xAI models are prefixed with "xai/" in models.json.
const lookupKey = model.provider === "xai" ? `xai/${modelId}` : modelId;
if (!(lookupKey in modelsJson) && !(modelId in modelsExtra)) {
// xAI and Moonshot models are provider-prefixed in token metadata.
const lookupKey =
model.provider === "xai" || model.provider === "moonshotai"
? `${model.provider}/${modelId}`
: modelId;
if (!(lookupKey in modelsJson) && !(lookupKey in modelsExtra) && !(modelId in modelsExtra)) {
missingModels.push(`${key}: ${model.provider}:${modelId}`);
}
}
Expand All @@ -43,6 +46,11 @@ describe("Known Models Integration", () => {
expect(MODEL_ABBREVIATIONS["gpt-5.5"]).toBeUndefined();
});

test("kimi aliases resolve to the direct Moonshot Kimi K3 model", () => {
expect(MODEL_ABBREVIATIONS.kimi).toBe("moonshotai:kimi-k3");
expect(MODEL_ABBREVIATIONS.k3).toBe("moonshotai:kimi-k3");
});

test("known model ids and aliases stay unique across the curated registry", () => {
const seenIds = new Set<string>();
const seenAliases = new Set<string>();
Expand Down
13 changes: 12 additions & 1 deletion src/common/constants/knownModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { formatModelDisplayName } from "../utils/ai/modelDisplay";

type ModelProvider = "anthropic" | "openai" | "google" | "xai" | "deepseek";
type ModelProvider = "anthropic" | "openai" | "google" | "xai" | "deepseek" | "moonshotai";

interface KnownModelDefinition {
/** Provider identifier used by SDK factories */
Expand Down Expand Up @@ -211,6 +211,17 @@ const MODEL_DEFINITIONS = {
aliases: ["deepseek-flash", "deepseek-v4-flash"],
tokenizerOverride: "deepseek/deepseek-v3.1",
},
// Kimi K3 - Moonshot AI's flagship open-weight multimodal reasoning model (released
// July 16, 2026; 1M context, text+image input; $3/M in, $15/M out).
// Bare `kimi` alias tracks Moonshot's flagship per the shortest-alias convention.
KIMI_K3: {
provider: "moonshotai",
providerModelId: "kimi-k3",
aliases: ["kimi", "k3", "kimi-k3"],
// K3's tokenizer isn't published in ai-tokenizer yet; reuse Kimi K2 (the newest
// Moonshot tokenizer available) for approximate counting.
tokenizerOverride: "moonshotai/kimi-k2",
},
} as const satisfies Record<string, KnownModelDefinition>;

export type KnownModelKey = keyof typeof MODEL_DEFINITIONS;
Expand Down
10 changes: 9 additions & 1 deletion src/common/constants/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type ProviderName =
| "google"
| "xai"
| "deepseek"
| "moonshotai"
| "openrouter"
| "github-copilot"
| "bedrock"
Expand Down Expand Up @@ -128,13 +129,20 @@ export const PROVIDER_DEFINITIONS = {
requiresApiKey: true,
kind: "direct",
},
moonshotai: {
displayName: "Moonshot AI",
import: () => import("@ai-sdk/moonshotai"),
factoryName: "createMoonshotAI",
requiresApiKey: true,
kind: "direct",
},
openrouter: {
displayName: "OpenRouter",
import: () => import("@openrouter/ai-sdk-provider"),
factoryName: "createOpenRouter",
requiresApiKey: true,
kind: "gateway",
routes: ["anthropic", "openai", "google", "xai", "deepseek"],
routes: ["anthropic", "openai", "google", "xai", "deepseek", "moonshotai"],
passthrough: false,
toGatewayModelId: toSlashSeparatedGatewayModelId,
fromGatewayModelId: fromSlashSeparatedGatewayModelId,
Expand Down
9 changes: 9 additions & 0 deletions src/common/types/thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,15 @@ export function openaiSupportsProMode(modelString: string): boolean {
return isGpt56FamilyModel(modelString);
}

/**
* Kimi K3 (Moonshot AI) always reasons and supports only the max reasoning
* effort; the thinking policy and the Moonshot/OpenRouter provider-options
* branches key off this predicate.
*/
export function isKimiK3Model(modelString: string): boolean {
return stripModelProviderPrefixes(modelString) === "kimi-k3";
}

/**
* Whether the given Anthropic model rejects `thinking: { type: "disabled" }`.
*
Expand Down
9 changes: 9 additions & 0 deletions src/common/utils/ai/modelCapabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ describe("getModelCapabilities", () => {
expect(caps).not.toBeNull();
});

it("reports image support for Kimi K3 via direct and gateway ids", () => {
for (const model of ["moonshotai:kimi-k3", "openrouter:moonshotai/kimi-k3"]) {
const caps = getModelCapabilities(model);
expect(caps).not.toBeNull();
expect(caps?.supportsVision).toBe(true);
expect(caps?.supportsPdfInput).toBe(false);
}
});

it("infers PDF support for OpenAI vision models when models-extra omits the flag", () => {
const caps = getModelCapabilities("openai:gpt-5.5");
expect(caps).not.toBeNull();
Expand Down
53 changes: 53 additions & 0 deletions src/common/utils/ai/providerOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,59 @@ describe("buildProviderOptions - Google", () => {
});
});

describe("buildProviderOptions - Moonshot", () => {
test("sends the explicit max reasoning effort for direct Kimi K3", () => {
expect(buildProviderOptions("moonshotai:kimi-k3", "max")).toEqual({
moonshotai: { reasoningEffort: "max" },
});
});

test("emits no provider options for other Moonshot models", () => {
expect(buildProviderOptions("moonshotai:kimi-k2.5", "medium")).toEqual({});
});
});

describe("buildProviderOptions - OpenRouter", () => {
test("sends the explicit max effort for OpenRouter-routed Kimi K3", () => {
// `enabled: true` alone falls back to OpenRouter's default (medium) effort,
// which K3 does not support, so the effort must be sent explicitly.
const result = buildProviderOptions(
"moonshotai:kimi-k3",
"max",
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
"openrouter"
);
expect(result).toEqual({
openrouter: {
reasoning: {
enabled: true,
effort: "max",
exclude: false,
},
},
});
});

test("maps thinking levels to effort for other OpenRouter reasoning models", () => {
expect(buildProviderOptions("openrouter:z-ai/glm-4.6", "medium")).toEqual({
openrouter: {
reasoning: {
enabled: true,
effort: "medium",
exclude: false,
},
},
});

expect(buildProviderOptions("openrouter:z-ai/glm-4.6", "off")).toEqual({});
});
});

describe("buildRequestHeaders", () => {
for (const { name, model, options, expected } of [
{
Expand Down
36 changes: 34 additions & 2 deletions src/common/utils/ai/providerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ANTHROPIC_THINKING_BUDGETS,
GEMINI_THINKING_BUDGETS,
getOpenAIReasoningEffort,
isKimiK3Model,
openaiSupportsProMode,
OPENROUTER_REASONING_EFFORT,
} from "@/common/types/thinking";
Expand Down Expand Up @@ -51,7 +52,9 @@ interface OpenRouterReasoningOptions {
reasoning?: {
enabled?: boolean;
exclude?: boolean;
effort?: "low" | "medium" | "high";
// "max" is not in OpenRouter's generic low/medium/high set; it is the only
// effort Kimi K3 accepts (model metadata supported_efforts: ["max"]).
effort?: "low" | "medium" | "high" | "max";
};
}

Expand All @@ -60,6 +63,15 @@ type OpenAICompatibleGatewayProviderOptions = Pick<
"reasoningEffort"
>;

/**
* Moonshot AI provider options (@ai-sdk/moonshotai). Kimi K3 accepts only the
* literal "max" reasoning effort, matching the SDK's typed options schema.
*/
interface MoonshotAIProviderOptions {
[key: string]: JSONValue | undefined;
reasoningEffort?: "max";
}

/**
* Provider-specific options structure for AI SDK
*/
Expand All @@ -68,6 +80,7 @@ type ProviderOptions =
| { openai: OpenAIResponsesProviderOptions }
| { google: GoogleGenerativeAIProviderOptions }
| { openrouter: OpenRouterReasoningOptions }
| { moonshotai: MoonshotAIProviderOptions }
| { xai: XaiProviderOptions }
| { "github-copilot": OpenAICompatibleGatewayProviderOptions }
| Record<string, never>; // Empty object for unsupported providers
Expand Down Expand Up @@ -472,9 +485,28 @@ export function buildProviderOptions(
return options;
}

// Build Moonshot-specific options
if (formatProvider === "moonshotai") {
// Kimi K3 always reasons and supports only the max reasoning effort. Send it
// explicitly rather than relying on the API default.
if (isKimiK3Model(capabilityModel)) {
const options = {
moonshotai: { reasoningEffort: "max" },
} satisfies { moonshotai: MoonshotAIProviderOptions };
log.debug("buildProviderOptions: Returning Moonshot options", options);
return options;
}
return {};
}

// Build OpenRouter-specific options
if (formatProvider === "openrouter") {
const reasoningEffort = OPENROUTER_REASONING_EFFORT[effectiveThinking];
// Kimi K3 always reasons and supports only the max reasoning effort. Send it
// explicitly: `enabled: true` alone falls back to OpenRouter's default (medium)
// effort, which the model does not support.
const reasoningEffort = isKimiK3Model(capabilityModel)
? "max"
: OPENROUTER_REASONING_EFFORT[effectiveThinking];

log.debug("buildProviderOptions: OpenRouter config", {
reasoningEffort,
Expand Down
Loading
Loading